- Dokumente in Angular: Upload & Download - 29. März 2019
- Externe Daten: Zentrales Statemanagement mit Redux - 28. März 2019
- Routenbasierte Daten mit Resolver laden - 27. März 2019
Redux
Ein andere Ansatz für das Halten & Verwalten von Daten ist ein zentrales State Management mit Redux. Redux hilft uns Dateninkonsistenz innerhalb unsere Anwendung zu vermeiden und Zustandsänderungen transparent sowie nachvollziehbar zu machen.
Der Zustand der Anwendung wird in einem nicht veränderbaren Store
gespeichert. Durch Actions
können Zustandsänderungen beauftragt werden. Der Reducer
verarbeitet die Actions
und errechnet aus dem alten Zustand sowie die Daten aus der Action
den neuen Zustand, der im Store gespeichert wird. Reducer
dürfen jedoch keine anderen Abhängigkeiten haben oder asynchrone Operationen ausführen.
Zu Interagieren mit externen Schnittstellen – wie dem Laden von externe Daten eignen sich deshalb Reducer
nicht. Effects
hören auf die Actions wie ein Reducer
– verändern jedoch nicht den Zustand. Wir können einen Effect
nutzen, um die Daten zuladen. Im Erfolgsfall lösen wir einen weitere Action
(Load_Success
) mit den geladen Daten aus. Ein Reducer
schreibt die Daten anschließend in den Store
.
Soviel zu den Grundlagen: Schauen wir uns den Programmcode an!
Vorbereitungen
Für den Einsatz von Redux in Angular gibt es verschiedene Frameworks:
- NgRx (Website: https://ngrx.io/, GitHub: https://github.com/ngrx/platform)
- NGXS (Website: http://ngxs.io/, GitHub: https://github.com/ngxs/store)
- Angular-Redux (GitHub: https://github.com/angular-redux/store)
In dieses Blogpost werden wir NgRx verwenden.
Für die Installation werden die folgenden Pakete zu deinem Angular-Projekt hinzufügt:
npm install --save @ngrx/store @ngrx/effects
Ziel der Anwendung: Wir wollen eine Liste an Blogpost von einer Schnittstelle laden. Dafür erstellen wir eine Entität-Klasse, die unsere Daten für einen Blogpost halten wird. Es ist die gleiche Entität-Klasse wie wir schon im Teil Externe Daten durch HTTP Client laden verwendet haben.
export class PostEntity {
userId: number;
id: number;
title: string;
body: string;
static fromJson(obj: object): PostEntity {
const newEntity = new PostEntity();
if (obj.hasOwnProperty('userId')) {
newEntity.userId = Number(obj['userId']);
}
if (obj.hasOwnProperty('id')) {
newEntity.id = Number(obj['id']);
}
if (obj.hasOwnProperty('title')) {
newEntity.title = obj['title'];
}
if (obj.hasOwnProperty('body')) {
newEntity.body = obj['body'];
}
return newEntity;
}
}
Zustand, Actions und Reducer
Definieren wir erstmal, was wir in unserem Zustand speichern wollen. Der Zustand ist nicht global über alle Objekte, die wir jemals verwenden wollen, sondern wir können für jede Teildomäne separat definieren. Wir wollen neben der Liste der Blogpost list: PostEntity[]
auch kennen, ob die Liste schon geladen wurde (initLoad
). Initial neben wir an, dass die Liste leer ist und noch geladen werden muss.
import {PostEntity} from './post.entity';
export class PostState {
initLoad = false;
list: PostEntity[] = [];
}
Zum Laden der Daten definieren wir uns drei Actions
:
Load
: Bitte Daten ladenLoadSuccess
: Laden war erfolgreich und die Daten sind bereit für denStore
LoadFail
: Laden war fehlerhaft
import {Action} from '@ngrx/store';
import {PostEntity} from './post.entity';
export enum PostActionTypes {
Load = '[Post] Load',
LoadSuccess = '[Post] Load Success',
LoadFail = '[Post] Load Fail',
}
export class PostLoadAction implements Action {
readonly type = PostActionTypes.Load;
}
export class PostLoadSuccessAction implements Action {
readonly type = PostActionTypes.LoadSuccess;
constructor(public posts: PostEntity[]) {
}
}
export class PostLoadFailAction implements Action {
readonly type = PostActionTypes.LoadFail;
}
Der Reducer
berechnet aus der Action
und dem alten Zustand, den neuen Zustand des Stores
.
- Bei
Load
undLoadFail
wird der Zustand nicht geändert. - Bei
LoadFail
wird eine Fehlermeldung über die Konsole ausgegeben – ohne den Zustand zu ändern.
Zur Vereinfachung haben wir an dieser Stelle auf eine Fehlermeldung an den Benutzer verzichtet. - Bei
LoadSuccess
werden die geladen Daten in die Liste gespeichert
und die MarkierunginitLoad = true
gesetzt.
Wichtig ist dabei die JavaScript Syntax const newState = {...state};
: Wir ändern den Zustand nie direkt, sondern arbeiten auf deiner Kopie. Die vorgestellte Schreibweise {...state}
– auch Object Spread Operator genannt – ist eine Kurzform von Object.assign({}, state)
.
export const initialState: PostState = new PostState();
export function postReducer(state = initialState, action: Action): PostState {
switch (action.type) {
case PostActionTypes.LoadSuccess: {
const newState = {...state};
newState.list = (action as PostLoadSuccessAction).posts;
newState.initLoad = true;
return newState;
}
case PostActionTypes.LoadFail:
{
console.error({error: action});
return state;
}
default:
return state;
}
}
Jetzt bringen wir alles zusammen: Das AppModule
benötigt noch einen Import für StoreModule.forRoot({post: postReducer})
. Die Konfiguration würde funktionieren – nur würden nie die Daten geladen werden. Deshalb schauen wir uns im nächsten Abschnitt den Effect
an. Im AppModule
haben wir den notwendigen Import EffectsModule.forRoot([ PostLoadEffect])
hinzugefügt.
@NgModule({
declarations: [
AppComponent,
CreateComponent,
ListComponent
],
imports: [
BrowserModule,
HttpClientModule,
FormsModule,
StoreModule.forRoot({post: postReducer}),
EffectsModule.forRoot([
PostLoadEffect,
])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
}
Effect: Daten vom Backend laden
Der Effect lädt die Daten von einer API. Schauen wir uns die Details an: Auf alle Actions
Benachrichtigungen gehört.
Da uns jedoch nur ein bestimmter Aktionstyp interessiert, filtern wir die Action
Benachrichtigungen mit ofType(PostActionTypes.Load)
. Nun betrachten wir im folgenden Programmcode nur die Actions PostActionTypes.Load
, die die Backenddaten laden soll.
Mit this.http.get('https://jsonplaceholder.typicode.com/posts')
holen wir uns die gewünschten Daten von der Schnittstelle. Wir transformieren im ersten Schritt in die gewünschte PostEntity
Klasse. Im zweiten Schritt bauen wir uns eine PostLoadSuccessAction
mit den gewünschten Daten. Im Fehlerfall möchten wir die PostLoadFailAction
ausführen, dass wir mit catchError(() => of(new PostLoadFailAction()))
definieren. Mit flatMap
bzw. mergeMap
führen wir den zweiten asynchronen Event Strom von this.http.get(...)
zusammen (siehe auch http://reactivex.io/documentation/operators/flatmap.html).
@Injectable()
export class PostLoadEffect {
// Hört auf die Aktion 'LOAD'
@Effect()
load$: Observable = this.action$.pipe(
ofType(PostActionTypes.Load),
mergeMap(action =>
this.http.get('https://jsonplaceholder.typicode.com/posts').pipe(
map((list: object[]) => list.map((e: object) => PostEntity.fromJson(e))),
map((list: PostEntity[]) => new PostLoadSuccessAction(list)),
catchError(() => of(new PostLoadFailAction()))
)
)
);
constructor(private http: HttpClient, private action$: Actions) {
}
}
Jetzt haben wir fast alles: Redux ist aufgesetzt. Jetzt fehlen noch zwei Punkte: Erstens es muss die Action Load
ausgelöst werden und zweitens wir müssen noch die Daten aus dem Store auslesen.
Load Action auslösen und Ergebnisse darstellen
Die Load
-Action lösen wir in unserem Beispiel beim Erstellen der Komponente (ngOnInit
) aus. Den Store
können wir mithilfe von store.pipe(select('post'));
auslesen. Die Referenz private store: Store<{ post: PostState }>
bekommen wir mithilfe der Dependency Injektion von Angular.
@Component({
selector: 'app-list',
templateUrl: './list.component.html',
styleUrls: ['./list.component.css']
})
export class ListComponent implements OnInit {
postState$: Observable;
constructor(private store: Store<{ post: PostState }>) {
this.postState$ = store.pipe(select('post'));
}
ngOnInit() {
this.store.dispatch(new PostLoadAction());
}
onDelete(post: PostEntity) {
this.store.dispatch(new PostDeleteAction(post));
}
}
Zum Darstellen des Observables postState$
nutzen wir die async
-Pipe.
Das Codebeispiel geht noch weiter und zeigt, wie Datensätze hinzugefügt sowie gelöscht werden können. Den Programmcode findest du unter https://github.com/dornsebastian/angular-ngrx.
Im nächsten Teil schauen wir uns an, wie wir Dateien hochladen und downloaden können.