A typical web application has various types of state
https://github.com/gothinkster/realworld“Yeah, that was a tricky slide [the one with multiple models and views and bidirectional data flow], partly because there's not a lot of consensus for what MVC is exactly - lots of people have different ideas about what it is. What we're really arguing against is bi-directional data flow, where one change can loop back and have cascading effects.”
@Injectable()
export class Store extends BehaviorSubject {
constructor(private dispatcher, private reducer, private initialState) {
super(initialState);
this.dispatcher.pipe(
tap(val => console.log('ACTION: ', val)),
scan((state, action) => this.reducer(state, action), initialState),
tap(val => console.log('STATE: ', val)),
).subscribe(state => super.next(state));
}
dispatch(action: any) {
this.dispatcher.dispatch(action);
}
select(key: string) {
return this.pipe(map(state => state[key]));
}
}
export class Dispatcher extends Subject {
dispatch(action: Action): void {
this.next(action);
}
}
Stream(loadMagazineEveryMonth$).pipe(
filter(magazine => magazine.month === 'April')
).subscribe(magazine => console.log(magazine.news));
// What: I want to retrieve only the April edition.
// How: I don't care..
Observable {
producer => events
push values => consumer(observer)
unsubscribe();
}
Subject {
producer => events
push values => list of consumers(observers)
unsubscribe();
next(); complete(); error(); // can be both Observable and Observer
}
Must read: Learning Observable By Building Observable / On The Subject Of Subjects (Ben Lesh)
imports: [
...
...
StoreModule.forRoot({ router: routerReducer },
{ metaReducers: !environment.production ? [storeFreeze] : [] }),
EffectsModule.forRoot([]),
!environment.production ? StoreDevtoolsModule.instrument() : [],
StoreRouterConnectingModule,
],
providers: [{ provide: RouterStateSerializer, useClass: CustomSerializer }],
export function freeze(reducer: ActionReducer): ActionReducer {
return function(state, action) {
const nextState = reducer(state, action);
Object.freeze(nextState);
return nextState;
}
}
@NgModule({
imports: [
StoreModule.forRoot(freeze(reducer))
]
})
export class AppModule {}
// running this in console we get
// 'Uncaught TypeError: Cannot assign to read only property 'test1''
(function()
{
'use strict';
console.log("FUN_1:");
let test = {test1: 'aha'};
test.test1 = 2;
Object.freeze(test);
test.test1 = 1;
}());
export interface SerializedRouterStateSnapshot {
root: ActivatedRouteSnapshot;
url: string;
}
export interface RouterStateUrl {
url: string;
params: Params;
queryParams: Params;
}
export interface State {
router: RouterReducerState;
}
export class CustomSerializer implements RouterStateSerializer {
serialize(routerState: RouterStateSnapshot): RouterStateUrl {
let route = routerState.root;
while (route.firstChild) {
route = route.firstChild;
}
const { url, root: { queryParams } } = routerState;
const { params } = route;
return { url, params, queryParams };
}
}
We can conrtol time in our store by replaying dispatched actions.
Here are our goals:
Let's see some examples of these requirements in the demo
export interface Auth {
loggedIn: boolean;
user: User;
status: Status;
}
export interface AuthState {
readonly auth: Auth;
}
export interface User {
email: string;
token: string;
username: string;
bio: string;
image: string;
}
export type Status = 'INIT' | 'IN_PROGRESS';
export const authInitialState: Auth = {
loggedIn: false,
status: 'INIT',
user: {
email: '',
token: '',
username: '',
bio: '',
image: ''
}
};
3. Next we create the reducer and register it to the module (libs/src/+state/auth.reducer.ts).
// (libs/src/+state/auth.reducer.ts)
export function authReducer(state: Auth, action: AuthAction): Auth {
switch (action.type) {
default: {
return state;
}
}
// (libs/src/auth.module.ts)
imports: [
...
StoreModule.forFeature('auth', authReducer, { initialState: authInitialState })
]
Our world until now was synchronous.
Generally, NgRx promotes functional programming. Reducers, selectors, RxJS operators are pure functions. This means that they produce no side effects.
But, side effects are impossible to avoid. We can isolate side effects (http calls, read from a file, write to localStorage) through ngrx/effects so that the rest of our ngrx implementation be composed of pure functions.
4. Next we create the effects class and register it to the module (libs/src/+state/auth.effects.ts).
// (libs/src/+state/auth.effects.ts)
@Injectable()
export class AuthEffects {
@Effect()
getUser ...
}
// (libs/src/auth.module.ts)
imports: [
...
StoreModule.forFeature('auth', authReducer, { initialState: authInitialState }),
EffectsModule.forFeature([AuthEffects])
]
lazy modules with ngrx v2
// (libs/auth/src/login/login.component.ts)
submit() {
this.store.dispatch(new fromActions.Login());
}
// (libs/auth/src/+state/auth.actions.ts)
import { Action } from '@ngrx/store';
export const enum AuthActionTypes {
LOGIN = '[auth] LOGIN',
LOGIN_SUCCESS = '[auth] LOGIN_SUCCESS',
LOGIN_FAIL = '[auth] LOGIN_FAIL'
...
}
// (libs/auth/src/+state/auth.effects.ts)
@Effect()
login = this.actions.ofType(AuthActionTypes.LOGIN).pipe(
withLatestFrom(this.store.select(fromNgrxForms.getData)),
exhaustMap(([action, data]) =>
this.authService.authUser('LOGIN', data).pipe(
map(result => new fromActions.LoginSuccess(result.user)),
catchError(result => of(new fromNgrxForms.SetErrors(result.error.errors)))
)
)
);
// (libs/auth/src/+state/auth.reducers.ts)
export function authReducer(state: Auth, action: AuthAction): Auth {
switch (action.type) {case AuthActionTypes.LOGIN:
...
case AuthActionTypes.REGISTER: {
return { ...state, status: 'IN_PROGRESS' };
}
case AuthActionTypes.REGISTER_SUCCESS:
case AuthActionTypes.LOGIN_SUCCESS: {
return {
...state,
loggedIn: true,
status: 'INIT',
user: action.payload
};
}
case AuthActionTypes.LOGIN_FAIL:
case AuthActionTypes.REGISTER_FAIL: {
return { ...state, status: 'INIT' };
}
...
@Effect({ dispatch: false })
loginRegisterSuccess =
this.actions.ofType(AuthActionTypes.LOGIN_SUCCESS, AuthActionTypes.REGISTER_SUCCESS).pipe(
tap(action => {
this.localStorageJwtService.setItem(action.payload.token);
this.router.navigateByUrl('/');
})
);
Selectors are just functions, that allow us, to get only the part of the application state, that we are interested in.Because selectors are pure functions, the last result can be returned when the arguments match without reinvoking your selector function. This can provide performance benefits.
export const getAuth = createFeatureSelector('auth');
export const getLoggedIn = createSelector(getAuth, (auth: Auth) => auth.loggedIn);
export const getUser = createSelector(getAuth, (auth: Auth) => auth.user);
A mnemonic from Mike Ryan and Brandon Robert @ ng-conf
and also...