-
Notifications
You must be signed in to change notification settings - Fork 0
Angular State Manegment with ngrx store
- Knowledge of how to set the store up with NGRX Store
src/
app/
components/
containers/
user.ts
effects/
user.ts
store/
actions/
server/
user.ts
local/
user.ts
models/
user.ts
reducers/
user.ts
You can re order this as you see fit. The Effects directory could be in the store directory for example.
For this example we will be creating the basic parts of a User
service.
-
Container
- Entry Point
- Houses the Components
- View Data
-
Models
- Represents a Row from a Table
-
Actions
- Does something to the State
-
Effects
- Makes external API Requests
-
Reducer
- Changes the local in browser state
The goal of this is to manage a user asynchronously with well defined data structures that are easy to maintain and manage by leverging:
- Http requests
Effects
- Local state updates
Reducer
- Data subscriptions (for viewing data)
-
Containers
&Components
-
The Container
is the entry point from a url request. For our User example our path will be /users
, when a user navigates to that path, angular will load the associated container. (Don't forget to build out the route in the routing module).
ng g component containers/{service name}
ie.
ng g component containers/user
Models are Objects that represent data returned by the API. That data also represents the database structure so the properties of the object will represent the column names of the table.
store/models/user.ts
export interface User {
id: number;
}
An Action is kind of like your boss telling you to do something. You are listening and waiting to be told to do something and you know exactly what to do when the time comes. "Hey stupid programmer! Add this user!" (actual quote). Actions tell our Effects
and Reducer
to update the state. That state can either be on the Server (through API requests Effects
) or in the local memory (Reducer
).
store/actions/
server/user.ts
local/user.ts
store/action/server/user.ts
import {Action} from "@ngrx/store";
import {User} from "../../models/user";
export const LOAD_ALL = '[User] Load All';
export const LOAD = '[User] Load';
export const ADD = '[User] Add';
export const SET = '[User] Set';
export const DELETE = '[User] Delete';
export class LoadAllUsersAction implements Action {
readonly type = LOAD_ALL;
}
export class LoadUserAction implements Action {
readonly type = LOAD;
constructor(public payload: number) {} // user id
}
export class AddUserAction implements Action {
readonly type = ADD;
constructor(public payload: User) {}
}
export class SetUserAction implements Action {
readonly type = SET;
constructor(public payload: User) {}
}
export class DeleteUserAction implements Action {
readonly type = DELETE;
constructor(public payload: number) {} // user id
}
export type Actions =
| LoadAllUsersAction
| LoadUserAction
| AddUserAction
| SetUserAction
| DeleteUserAction;
store/action/local/user.ts
import {Action} from "@ngrx/store";
import {User} from "../../models/user";
export const ADD = '[User] Add';
export const SET = '[User] Set';
export const DELETE = '[User] Delete';
export class AddUserAction implements Action {
readonly type = ADD;
constructor(public payload: User) {}
}
export class SetUserAction implements Action {
readonly type = SET;
constructor(public payload: User) {}
}
export class DeleteUserAction implements Action {
readonly type = DELETE;
constructor(public payload: number) {} // user id
}
export type Actions =
| AddUserAction
| SetUserAction
| DeleteUserAction;
Notice how we don't have any Local Load
actions. These are done through external calls to the server.
Effects make external calls to API endpoints to update/retrieve data from the server then call associated Local Actions
to update the local store.
effects/user.ts
import {Injectable} from "@angular/core";
import {Actions, Effect} from "@ngrx/effects";
import {Observable} from "rxjs/Observable";
import {Action} from "@ngrx/store";
import * as userServerActions from "./../store/actions/server/user";
import * as userLocalAction from "./../store/actions/local/user";
import * as errors from '../store/actions/errors';
import {ApiService} from "../services/api.service";
import {of} from "rxjs/observable/of";
@Injectable()
export class UserEffects {
constructor(private actions$: Actions, private apiService: ApiService) {}
@Effect()
newUser$: Observable<Action> = this.actions$
.ofType(userServerActions.ADD)
.switchMap(a => {
// cast the action so we don't get type script errors
let action: userServerActions.AddUserAction = <userServerActions.AddUserAction>a;
// listen for more events coming in
const next$ = this.actions$.ofType(userServerActions.ADD).skip(1);
// call the api service
return this.apiService.addUser(action)
.takeUntil(next$)
.map(id => {
// set the local store data
return new userLocalAction.AddUserAction(Object.assign({}, action.payload, {id: id}))
})
.catch(e => of(new errors.ApiErrorAction(e)))
})
}
The ApiService class is a class that handles calling the built in Angular's built in http class, returning data, and prompting the user that we are loading.
services/api.service.ts
addUser(action: user.AddUserAction): Observable<number> {
this.startLoading(action.type);
return this.http.post(`${this.API_PATH}/users`, action.payload)
.map(res => res.json().data.id)
.catch(e => this.handleError(e))
.finally(() => this.endLoading(action.type))
}
store/reduces/user.ts
import {User} from "../models/user";
import * as user from "../actions/local/user";
export interface State {
entities: {[id: number]: User},
selectedUserId: number;
}
export const initialState: State = {
entities: {},
selectedUserId: 0,
};
export function reducer(state = initialState, action: user.Actions) {
switch (action.type) {
case user.ADD:
let newUser = {};
newUser[action.payload.id] = action.payload;
return Object.assign({}, state, {entities: Object.assign({}, state.entities, newUser)})
}
}
export const getUsers = (state: State) => state.entities;
Add the new reducer to the index reducer.
import * as fromUser from './users';
export interface State {
...
user: fromUser.State;
}
const reducers = {
...
user: fromUser.reducer,
};
export const getUserState = (state: State) => state.user;
export const getUsers = createSelector(getUserState, fromUser.getUsers);
Now we can finish things off by retrieving our loaded users from from the store, or dispatching a load request in our user
container.
import { Component, OnInit } from '@angular/core';
import {Store} from "@ngrx/store";
import * as fromRoot from "./../../store/reducers/index";
import * as serverUser from "./../../store/actions/server/user";
import {User} from "../../store/models/user";
@Component({
selector: 'app-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.css']
})
export class UsersComponent implements OnInit {
users: {[id:number]: User} = {};
constructor(private store: Store<fromRoot.State>) { }
ngOnInit() {
this.store.select(fromRoot.getUsers).subscribe(users => {
if (!users || !Object.keys(users).length) {
// dispatch load
this.store.dispatch(new serverUser.LoadAllUsersAction())
} else {
this.users = users
}
})
}
}