- Modern TS
- Little boilerplate
- Better types
- Selectors
- Easy Testing
- Crud API Wrapper
- ASYNC Operation
- Redux Debugger Integration
- Lazy Loadable Stores
- Meta/High Order Stores
- Schematics
- Keep read / writes decoupled
- Route Reducer
- Page Title Reducer
Redux is a great state mangement system, it provides:
- Decoupling state from components for better responsibility delegation
- Global state that components can share anywhere in the component tree
- Hot reload capibility
- Time travel capability
Redux was created for React which doesn't use Observables or TypeScript in its paradigms. NGRX is evolution of Redux that adds observables and Angular DI but it still keeps all the same paradigms as Redux.
Since Redux's paradigms weren't designed for these new technologies it has some shortfalls that we can exploit with this new capabilities like: types, decorators, observables, di.
Lets take the following example of making a simple request to populate a store with a HTTP request using NGRX:
export const enum ActionTypes {
LOAD = 'Loading',
LOAD_SUCCESS = 'Load Success',
LOAD_ERROR = 'Load Error'
}
export class Load implements Action {
readonly type = ActionTypes.LOAD;
constructor(public payload?: { limit?: number; offset?: number; filter?: any[]; sort?: any[] }) {}
}
export class LoadSuccess implements Action {
readonly type = ActionTypes.LOAD_PLATFORMS_SUCCESS;
constructor(public payload: MyModel) {}
}
export class LoadError implements Action {
readonly type = ActionTypes.LOAD_ERROR;
constructor(public payload: MyError) {}
}
export interface PizzaState {
loading: boolean;
items: any[];
errors: any[];
}
const initialState: PizzaState = {
loading: false,
items: [],
errors: null
};
export function pizzaReducer(state: PizzaState = initialState, action: PizzaActions) {
switch (action.type) {
case ActionTypes.LOAD:
return { ...state, loading: true };
case ActionTypes.LOAD_SUCCESS:
return { ...state, loading: false, items: action.payload };
case ActionTypes.LOAD_ERROR:
return { ...state, loading: false, errors: action.payload };
default:
return state;
}
}
@Injectable()
export class PizzaEffects {
constructor(
private store: Store<any>,
private update$: Actions,
private myService: MyService
) {}
@Effect()
loadPizzas$ = this.update$.ofType(ActionTypes.LOAD).pipe(
switchMap(state => this.myService.query()),
map(res => new LoadSuccess(res)));
This decouplation of read/write and async/sync state is very powerful but it creates a lot of code to do something rather simple. The goal of this project is to reduce this boilerplate down to simple dispatch and state action system.
People will want to abuse classes and might do bad things like put state in the class manually/etc.
Whats the best way to handle lazy loading?
Could be accomplished with inheritance (ppl w/ probably not like) or passed via decorator prop.
@Store<PizzaState>({
pizzas: [],
cooking: false
})
export class PizzaReducer extends DoughReducer { }
-- or --
@Store<PizzaState>({
pizzas: [],
cooking: false
}, DoughReducer, ...)
export class PizzaReducer { }
Do we keep effects decoupled, could we integrate them, or do we even need them now? vuex integrates them which could be interesting.
@Store<PizzaState>({
pizzas: [],
cooking: false
})
export class PizzaReducer {
// Could make injectable now that its a class
constructor(private pizzaSvc: PizzaService) {}
// Ability to return sync and async state from same action
@Action(BuyPizza)
buyPizza({ dispatch }, pizza) {
// not really a fan of passsing dispatch like this but not sure better way w/o inheritance
// this how vuex actually does it though...
const state = { ...this.state, cooking: true };
this.pizzaSvc.order(pizza)
.pipe(tap(res => dispatch(new PizzaOrdered(res)));
return state;
}
// Make actions accept observable responses
@Action(BuyPizza)
buyPizza$ = this.pizzaSvc.order(pizza)
.pipe(mergeMap(pizzas => { ...this.state, pizzas }))
// Maybe make a commit fn?
@Action(CookPizza)
cookPizza({ state, commit }, payload: Pizza): PizzaState {
commit({ ...this.state, cooking: true });
return this.pizzaService.get(payload).map(res => { ...this.state, cooking: false });
}
}
Key things:
- Should be able to return observables
- Should be able to do both sync and async ops in one action
Make a store that automatically has all crud ops and can hook up to a service contract.
export class PizzaService implements EntityService {
query();
get(id);
create(body);
update(body);
delete(id);
}
...then...
@EntityStore(PizzaService, { ... })
export class PizzaReducer {}
...then...
this.store.dispatch(new CreateEntity(body));
Question is how do we associate the generic entity to the store we care about w/o having to make a bunch of boilerplate to tie them together.
Example of current ngrx route reducer:
@Injectable()
export class RouterEffects {
constructor(private actions$: Actions, private router: Router, private location: Location) {}
@Effect({ dispatch: false })
navigate$ = this.actions$
.ofType(RouterActionTypes.GO)
.pipe(
map((action: RouterGo) => action.payload),
tap(({ path, queryParams, extras }) => this.router.navigate(path, { queryParams, ...extras }))
);
@Effect({ dispatch: false })
navigateBack$ = this.actions$.ofType(RouterActionTypes.BACK).pipe(tap(() => this.location.back()));
@Effect({ dispatch: false })
navigateForward$ = this.actions$.ofType(RouterActionTypes.FORWARD).pipe(tap(() => this.location.forward()));
}
Example of ngrx page title:
@Injectable()
export class TitleEffects {
constructor(private actions$: Actions, private titleService: Title) {}
@Effect({ dispatch: false })
navigate$ = this.actions$
.ofType(TitleActionTypes.SET)
.pipe(
map((action: SetTitle) => action.payload),
tap((name: string) => this.titleService.setTitle(`${name} | Pizza Store`))
);
}
You could combine Actions if you extend them (ya ya), for example:
class BuyPizza implements Action {}
class BuyPeporniPizza extends BuyPizza {}
In this scenario, you could make BuyPizza
be trigger when you dispatch BuyPeporniPizza
too.
NGRX Example of an Effect calling other Effects