Table of Contents
- @ngrx/store
- @ngrx/effects
- @ngrx/router-store
- Check Store
- @ngrx/entity
- @ngrx/store-devtools
- Resources
- Look for more
Managing state is hard.
@ngrx/store
provides predictable state management using actions and reducers.
Store can be thought of as a client-side ‘single source of truth’
, or a client-side database
.
A snapshot of store at any point supply a complete representation of the relevant application state.
Just to give an idea, it's not a real implementation!
class Dispatcher extends Rx.Subject {
dispatch(value: any): void {
this.next(value);
}
}
class Store<State> extends Rx.BehaviorSubject<State> {
constructor(
private dispatcher: Dispatcher,
private reducer,
initialState = {}
){
super(initialState);
this.dispatcher
.scan((state, action) => this.reducer(state, action), initialState)
.subscribe(state => super.next(state));
}
dispatch<V extends Action = Action>(action: V) {
this.dispatcher.dispatch(action);
}
select<T>(mapFn : (state: State) => T | string): Observable<T> {
return this
.map(state => state[key])
.distinctUntilChanged();
}
}
// Select is also available as lettable operator for rxjs.
function select<T>(mapFn : (state: State) => T | string): Observable<T>;
Each application built around store contain three main pieces:
- Actions.
- Reducers.
- Single application store.
Store is immutable (all changes produce new objects (shallow copies, not deep copies)).
- Centralized state (single source of truth) allows to observe the current data state from anywhere in the app.
- Isolation of data (data flows one-way).
- Predictable state management (all mutations are explicit).
- Performant (allows to disable Angular's change detection).
- Debuggable (time lapse sequence of state changes can be compared one to another).
- Testable (reducers are pure functions, components are simpler).
- Root and feature module support.
- Learning Curve.
- Integrating pattern with 3rd party packages can be difficult.
"we don't intend Redux to be used for all state. Just whatever seems significant to the app. I would argue inputs and animation state should be handled by state abstraction. Redux works better for things like fetched data and locally modified models." by @gaearon
Any time you want to change the state - you have to dispatch an action.
Action describes something that has/should happen, but doesn't specify how.
interface Action {
type: string;
payload?: any;
}
Note: payload?: any;
allowed to be named differently and you can supply additional properties.
// example.action.ts
import { Action } from '@ngrx/store';
// --- 1. Create string const for action. ---
// Actions values has to be unique and easily readable.
// So using namespace, e.g. `[Movie] Add`, might be a good idea.
// Tip: you can use current module name for namsespace.
// Tip#2: you can use enum instead of constants (see complex example).
export const UPDATE = '[Person] Update';
// --- 2. Create class for action. ---
// Usage of classes is determined by unlocking TypeScript's compile-time checking
// and triggering IDE autocompletion.
export class Update implements Action {
readonly type = UPDATE;
constructor(public payload: string) {}
}
// --- 3. Export all actions. ---
export type ExampleAction
= Update;
// | AnotherAction
// | AnotherAction...
// customers.action.ts
import { Action } from '@ngrx/store';
import { Update } from '@ngrx/entity';
import { Customer } from '../../models/customer.model';
export enum CustomersActionTypes {
LOAD_CUSTOMERS = '[Users] Load Customers',
LOAD_CUSTOMERS_FAIL = '[Users] Load Customers Fail',
LOAD_CUSTOMERS_SUCCESS = '[Users] Load Customers Success',
CREATE_CUSTOMER = '[Users] Create Customer',
CREATE_CUSTOMER_FAIL = '[Users] Create Customer Fail',
CREATE_CUSTOMER_SUCCESS = '[Users] Create Customer Success',
// ...Other CRUD actions.
}
// Loading customers.
export class LoadCustomers implements Action {
readonly type = CustomersActionTypes.LOAD_CUSTOMERS;
}
export class LoadCustomersFail implements Action {
readonly type = CustomersActionTypes.LOAD_CUSTOMERS_FAIL;
constructor(public payload: any) {}
}
export class LoadCustomersSuccess implements Action {
readonly type = CustomersActionTypes.LOAD_CUSTOMERS_SUCCESS;
constructor(public payload: { customers: Customer[] }) {}
}
// Creating customers.
export class CreateCustomer implements Action {
readonly type = CustomersActionTypes.CREATE_CUSTOMER;
constructor(public payload: { customer: Customer }) {}
}
export class CreateCustomerFail implements Action {
readonly type = CustomersActionTypes.CREATE_CUSTOMER_FAIL;
constructor(public payload: any) {}
}
export class CreateCustomerSuccess implements Action {
readonly type = CustomersActionTypes.CREATE_CUSTOMER_SUCCESS;
constructor(public payload: { customer: Customer }) {}
}
// ...Other CRUD actions.
// Exporting all actions.
export type CustomersAction
= LoadCustomers | LoadCustomersFail | LoadCustomersSuccess
| CreateCustomer | CreateCustomerFail | CreateCustomerSuccess
;
Reducer is a pure function that takes the previous state and an action and synchronously returns the new state.
Combination of reducers makes up a representation of application state at any given time.
- rely only on it's arguments;
- doesn't produce observable side-effects;
- doesn't modify input values (creates new ones when required).
interface Reducer<State> {
(state: State, action: Action): State;
}
- Is a pure function.
- Focused on a specific section, or slice of state (similar to a table in a database).
- Always returns a representation of state.
// example.reducer.ts
// --- 1. Import corresponding actions. ---
import * as fromExample from './example.actions';
// --- 2. Create interface of current part of state. ---
export interface ExampleState {
name: string;
}
// --- 3. Create initial state. ---
const initialState: ExampleState = {
name: ''
};
// --- 4. Create reducer. ---
export function reducer(state: ExampleState = initialState, action: fromExample.ExampleAction): ExampleState {
switch (action.type) {
case fromExample.UPDATE: {
return {name: action.payload};
}
default: {
return state;
}
}
}
// --- 5. Create selectors for each property in interface. ---
export const selectUserName = (state: ExampleState) => state.name;
// customers.reducer.ts
import { CustomersAction, CustomersActionTypes } from '../actions/customers.action';
import { Customer } from '../../models/customer.model';
export interface CustomerState {
entities: { [id: number]: Customer };
loaded: boolean;
loading: boolean;
}
export const initialState: CustomerState = {
entities: {},
loaded: false,
loading: false,
};
export function reducer(state = initialState, action: CustomersAction): CustomerState {
switch (action.type) {
case CustomersActionTypes.LOAD_CUSTOMERS: {
return {
...state,
loading: true
};
}
case CustomersActionTypes.LOAD_CUSTOMERS_SUCCESS: {
const customers = action.payload;
const entities = customers.reduce((allEntities: { [id: number]: Customer }, customer: Customer) => {
return {
...allEntities,
[customer.id]: customer
};
}, {...state.entities});
return {
...state,
loading: false,
loaded: true,
entities,
};
}
case CustomersActionTypes.LOAD_CUSTOMERS_FAIL: {
return {
...state,
loading: false,
loaded: false
};
}
// One of `switch`'s benefits is preventing duplication.
case CustomersActionTypes.CREATE_CUSTOMER_SUCCESS:
case CustomersActionTypes.UPDATE_CUSTOMER_SUCCESS: {
const customer = action.payload;
const entities = {
...state.entities,
[customer.id]: customer,
};
return {
...state,
entities,
};
}
case CustomersActionTypes.REMOVE_CUSTOMER_SUCCESS: {
const customer = action.payload;
// Desctucturing to remove.
const {[customer.id]: removed, ...entities} = state.entities;
return {
...state,
entities
};
}
default: {
return state;
}
}
}
export const selectCustomersEntities = (state: CustomerState) => state.entities;
export const selectCustomersLoaded = (state: CustomerState) => state.loaded;
export const selectCustomersLoading = (state: CustomerState) => state.loading;
Selectors - methods used for obtaining slices of store state.
interface Selector<AppState, SelectedState> {
(state: AppState): SelectedState;
}
- Reduce responsibility of components.
- Can be shared across the entire app.
When using the createSelector
and createFeatureSelector
functions @ngrx/store keeps track of the latest arguments in which your selector function was invoked.
Because selectors are pure functions, the last result can be returned when the arguments match without reinvoking selector function. This can provide performance benefits (memoization).
Is a convenience method for returning a top level feature state. Returns a typed selector function for a feature slice of state.
// store/reducers/index.ts
import { createFeatureSelector } from '@ngrx/store';
export const selectUsersState = createFeatureSelector<UsersState>('users');
// users.module.ts
@NgModule({
imports: [
StoreModule.forFeature('users', reducers),
],
})
Returns a callback function for selecting a slice of state.
import { createSelector, createFeatureSelector } from '@ngrx/store';
export interface FeatureState {
counter: number;
}
export const selectFeature = createFeatureSelector<FeatureState>('feature');
export const selectFeatureCount = createSelector(selectFeature, (state: FeatureState) => state.counter);
When the selector is invoked with the same arguments it will return the memoized value. A selector's memoized value stays in memory indefinitely.
If the memoized value is no longer needed it's possible to reset the memoized value to null to release it from memory by invoking the release
method on the selector.
// Memoized value of selectSome is now null.
selectSome.release()
// customers.selector.ts
import { createSelector } from '@ngrx/store';
import { Customer } from '../../models/customer.model';
import * as fromRoot from '../../../store';
import * as fromFeature from '../reducers';
import * as fromCustomers from '../reducers/customers.reducer';
import * as fromProducts from './products.selectors';
export const selectCustomersState = createSelector(fromFeature.selectUsersState, (state: fromFeature.UsersState) => state.customers);
export const selectCustomersEntities = createSelector(selectCustomersState, fromCustomers.selectCustomersEntities);
export const selectSelectedCustomer = createSelector(selectCustomersEntities, fromRoot.selectRouterState, (entities, router): Customer => {
return router.state && entities[router.state.params.customerId];
});
export const selectAllCustomers = createSelector(selectCustomersEntities, (entities) => {
return Object.keys(entities).map(id => entities[id]);
});
export const selectCustomersLoaded = createSelector(selectCustomersState, fromCustomers.selectCustomersLoaded);
export const selectCustomersLoading = createSelector(selectCustomersState, fromCustomers.selectCustomersLoading);
export const selectCustomerVisualised = createSelector(
selectSelectedCustomer,
fromProducts.selectProductsEntities,
fromProducts.selectSelectedProducts,
(customer, productsEntities, selectedProducts) => {
const products = selectedProducts.map(id => productsEntities[id]);
return {...customer, products};
}
);
import * as fromStore from '../../store';
ngOnInit() {
this.customers$ = this.store.select<Customer[]>(fromStore.selectAllCustomers);
}
// root index.ts
export * from './actions';
export * from './effects';
export * from './reducers';
export * from './selectors';
// actions/index.ts
// selectors/index.ts
export * from './each-file';
// effects/index.ts
import { SomeEffects } from './each.effect';
export const effects: any[] = [
SomeEffects,
// ...
];
export * from './each.effect';
// some.module.ts
import { EffectsModule } from '@ngrx/effects';
import { effects } from './store';
@NgModule({
imports: [
EffectsModule.forFeature(effects),
],
})
export class SomeModule { }
// reducers/index.ts
import { ActionReducerMap, createFeatureSelector } from '@ngrx/store';
// Importing all reducers from current folder.
import * as fromCustomers from './customers.reducer';
import * as fromProoducts from './products.reducer';
// Creating interface for feature (current module).
export interface UsersState {
customers: fromCustomers.CustomerState;
products: fromProoducts.ProductsState;
}
// Creating object with imported reducers using feature interface and ActionReducerMap.
export const reducers: ActionReducerMap<UsersState> = {
customers: fromCustomers.reducer,
products: fromProoducts.reducer,
};
// Creating feature selector for current module to use in selectors.
export const selectUsersState = createFeatureSelector<UsersState>('users');
// some.module.ts
import { StoreModule } from '@ngrx/store';
import { reducers } from './store';
@NgModule({
imports: [
StoreModule.forFeature('users', reducers),
],
})
export class SomeModule { }
import { Store } from '@ngrx/store';
import * as fromStore from '../../store';
import { SomeModel } from '../../models/some.model';
@Component({})
export class ExampleComponent {
constructor(private store: Store<fromStore.State>) {}
setExample(input: SomeModel): void {
this.store.dispatch(new fromStore.ActionName(input));
}
getExample(): Observable<SomeModel[]> {
return this.store.select<SomeModel[]>(fromStore.selectResults);
}
}
Components in Store application falls into one of two categories: smart or dumb.
- routable components at root level;
- have direct access to the store (or to a derivative);
- handle view events and the dispatching of actions (through a service or directly);
- handle the logic behind events emitted up from child components within the same view.
- generally for presentation only;
- rely exclusively on
@Input
parameters; - invokes callbacks via
@Output
; - when relevant events occur in dumb components, they are emitted up to be handled by a parent smart component;
- make up the majority of your application;
- should be small, focused, and reusable.
When components rely only on @Input
parameters, and those input references do not change,
it is possible to tell Angular to skip running change detection
(until there is a change in input references) to provide performance benefit.
To enable this utilize the changeDetectionStrategy
of OnPush
.
Note: It can be used on container components too (if component relies only on observables).
The AsyncPipe can handle Observables
and Promises
in template.
{{ stream$ | async }}
This pipe also handles unsubscribing (no need to manually cleaning up subscriptions on destroy).
@ngrx/store composes map of reducers into a single reducer.
metaReducers
configuration option allows to provide an array of meta-reducers that are composed from right to left.
// meta-reducers.ts
import { ActionReducer, MetaReducer } from '@ngrx/store';
import { reducers } from './reducers';
// console.log all actions
export function debug(reducer: ActionReducer<any>): ActionReducer<any> {
return function(state, action) {
console.log('state', state);
console.log('action', action);
return reducer(state, action);
}
}
export const metaReducers: MetaReducer<any>[] = [debug];
// app.module.ts
import { StoreModule } from '@ngrx/store';
import { metaReducers } from './meta-reducers';
import { reducers } from './reducers';
@NgModule({
imports: [
StoreModule.forRoot(reducers, { metaReducers })
]
})
export class AppModule {}
Manages side effects - code that interacts with the outside world (HTTP, web sockets, offline storage, etc.).
Main point is to listen to @ngrx/store action and return a new action.
Isolates side effect from components, so components becomes easier to test.
Actions
is an Observable to inject in a constructor.
import { Actions } from '@ngrx/effects';
constructor(private actions$: Actions) { }
export declare function ofType<T extends Action>(...allowedTypes: string[]): (source$: Actions<T>) => Actions<T>;
@Effect
is a decorator to hint @ngrx/effects that we want to subscribe to some properties.
When there is no need to dispatch a new action from the effect:
@Effect({dispatch: false})
// customers.effects.ts
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { map, switchMap, catchError } from 'rxjs/operators';
import { of } from 'rxjs/observable/of';
import * as fromRoot from '../../../store';
import * as fromServices from '../../services';
import * as customerActions from '../actions/customers.action';
import { CustomersActionTypes } from '../actions/customers.action';
@Injectable()
export class CustomersEffects {
constructor(private actions$: Actions,
private customersService: fromServices.CustomersService) {
}
@Effect()
loadCustomers$ = this.actions$.pipe(
ofType<customerActions.LoadCustomers>(CustomersActionTypes.LOAD_CUSTOMERS),
switchMap(() => {
return this.customersService.getCustomers().pipe(
map(customers => new customerActions.LoadCustomersSuccess(customers)),
catchError(error => of(new customerActions.LoadCustomersFail(error)))
);
})
);
@Effect()
createCustomer$ = this.actions$.pipe(
ofType<customerActions.CreateCustomer>(CustomersActionTypes.CREATE_CUSTOMER),
map(action => action.payload),
switchMap(customer => {
return this.customersService.createCustomer(customer).pipe(
map(createdCustomer => new customerActions.CreateCustomerSuccess(createdCustomer)),
catchError(error => of(new customerActions.CreateCustomerFail(error)))
);
})
);
@Effect()
createCustomerSuccess$ = this.actions$.pipe(
ofType<customerActions.CreateCustomerSuccess>(CustomersActionTypes.CREATE_CUSTOMER_SUCCESS),
map(action => action.payload),
map(customer => new fromRoot.Go({
path: ['/users', customer.id]
})),
);
@Effect()
updateCustomer$ = this.actions$.pipe(
ofType<customerActions.UpdateCustomer>(CustomersActionTypes.UPDATE_CUSTOMER),
map(action => action.payload),
switchMap(customer => {
return this.customersService.updateCustomer(customer).pipe(
map(updatedCustomer => new customerActions.UpdateCustomerSuccess(updatedCustomer)),
catchError(error => of(new customerActions.UpdateCustomerFail(error)))
);
})
);
@Effect()
removeCustomer$ = this.actions$.pipe(
ofType<customerActions.RemoveCustomer>(CustomersActionTypes.REMOVE_CUSTOMER),
map(action => action.payload),
switchMap(customer => {
return this.customersService.removeCustomer(customer).pipe(
map(() => new customerActions.RemoveCustomerSuccess(customer)),
catchError(error => of(new customerActions.RemoveCustomerFail(error)))
);
})
);
@Effect()
handleCustomerSuccess$ = this.actions$.pipe(
ofType<customerActions.RemoveCustomerSuccess | customerActions.UpdateCustomerSuccess>
(CustomersActionTypes.REMOVE_CUSTOMER_SUCCESS, CustomersActionTypes.UPDATE_CUSTOMER_SUCCESS),
map(customer => new fromRoot.Go({
path: ['/users'],
}))
);
}
Binds Angular Router State to Application State (@ngrx/store) , which gives more power to compose the State as a single source of truth.
@ngrx/router-store is listening to Angular routing events and updating store with current route (using custom serializer).
As an additional benefit, we can obliterate Angular Router
as a DI in the component constructor.
Class (usually in reducers/index.ts
) with serialize
method to transform Angular RouterStateSnapshot
into a RouterStateUrl
to bind to the Store.
@ngrx/router-store setup is done in root app module.
import { Action } from '@ngrx/store';
import { NavigationExtras } from '@angular/router';
export enum RouterActionTypes {
GO = '[Router] Go',
BACK = '[Router] Back',
FORWARD = '[Router] Forward',
}
export class Go implements Action {
readonly type = RouterActionTypes.GO;
constructor(public payload: {
path: any[];
query?: object,
extras?: NavigationExtras
}) {}
}
export class Back implements Action {
readonly type = RouterActionTypes.BACK;
}
export class Forward implements Action {
readonly type = RouterActionTypes.FORWARD;
}
export type RouterActions = Go | Back | Forward;
import { ActivatedRouteSnapshot, Params, RouterStateSnapshot } from '@angular/router';
import { ActionReducerMap, createFeatureSelector } from '@ngrx/store';
import * as fromRouter from '@ngrx/router-store';
export interface RouterStateUrl {
// Full url.
url: string;
// `?param=value`
queryParams: Params;
// `/:id`
params: Params;
}
export interface State {
routerReducer: fromRouter.RouterReducerState<RouterStateUrl>;
}
export const reducers: ActionReducerMap<State> = {
routerReducer: fromRouter.routerReducer
};
export const selectRouterState = createFeatureSelector<fromRouter.RouterReducerState<RouterStateUrl>>('routerReducer');
export class CustomSerializer implements fromRouter.RouterStateSerializer<RouterStateUrl> {
serialize(routerState: RouterStateSnapshot): RouterStateUrl {
// ES6 syntax for `const url = routerState.url;`
const {url} = routerState;
const {queryParams} = routerState.root;
let state: ActivatedRouteSnapshot = routerState.root;
while (state.firstChild) {
state = state.firstChild;
}
const {params} = state;
return {url, queryParams, params};
}
}
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Router } from '@angular/router';
import { Location } from '@angular/common';
import { map, tap } from 'rxjs/operators';
import { RouterActionTypes } from '../actions/router.action';
import * as routerActions from '../actions/router.action';
@Injectable()
export class RouterEffects {
constructor(private actions$: Actions,
private router: Router,
private location: Location) {
}
@Effect({dispatch: false})
navigate$ = this.actions$.pipe(
ofType<routerActions.Go>(routerActions.RouterActionTypes.GO),
map(action => action.payload),
tap(({path, query: queryParams, extras}) => {
this.router.navigate(path, {queryParams, ...extras});
})
);
@Effect({dispatch: false})
navigateBack$ = this.actions$.pipe(
ofType<routerActions.Back>(RouterActionTypes.BACK),
tap(() => this.location.back())
);
@Effect({dispatch: false})
navigateForward$ = this.actions$.pipe(
ofType<routerActions.Forward>(RouterActionTypes.FORWARD),
tap(() => this.location.forward())
);
}
import { StoreRouterConnectingModule, RouterStateSerializer } from '@ngrx/router-store';
import { CustomSerializer } from './store';
@NgModule({
imports: [
StoreRouterConnectingModule,
],
providers: [{provide: RouterStateSerializer, useClass: CustomSerializer}],
})
export class AppModule { }
import * as fromRoot from '../../../store';
@Effect()
handleCustomerSuccess$ = this.actions$.pipe(
ofType<customerActions.RemoveCustomerSuccess | customerActions.UpdateCustomerSuccess>
(CustomersActionTypes.REMOVE_CUSTOMER_SUCCESS, CustomersActionTypes.UPDATE_CUSTOMER_SUCCESS),
map(customer => new fromRoot.Go({
path: ['/users'],
}))
);
To ensure that required Store keys is available on a current route (even if page was refreshed) we can use Angular Route Guards.
// customer-exists.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot } from '@angular/router';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { filter, take, tap, map, switchMap } from 'rxjs/operators';
import { Customer } from '../models/customer.model';
import * as fromStore from '../store';
@Injectable()
export class CustomerExistsGuard implements CanActivate {
constructor(private store: Store<fromStore.UsersState>) {
}
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
return this.checkStore().pipe(
switchMap(() => {
const id = parseInt(route.params.customerId, 10);
return this.hasCustomer(id);
}),
);
}
hasCustomer(id: number): Observable<boolean> {
return this.store.pipe(
select(fromStore.selectCustomersEntities),
map((entities: { [key: number]: Customer }) => Boolean(entities[id])),
// Unsubscribe automatically.
take(1)
);
}
// Method is similar across all Store-dealing guards.
checkStore(): Observable<boolean> {
return this.store.pipe(
select(fromStore.selectCustomersLoaded),
tap(loaded => {
if (!loaded) {
this.store.dispatch(new fromStore.LoadCustomers());
}
}),
// Waits for loaded become true.
filter((loaded: boolean) => loaded),
// Unsubscribe automatically when loaded.
take(1)
);
}
}
Note: select is available for usage as operator to pipe on store, as seen in this example.
import { SomeGuard } from './each.guard';
export const guards: any[] = [
SomeGuard,
// ...
];
export * from './each.guard';
import { NgModule } from '@angular/core';
// services
import * as fromServices from './services';
// guards
import * as fromGuards from './guards';
@NgModule({
providers: [...fromServices.services, ...fromGuards.guards],
})
export class SomeModule { }
// guards
import * as fromGuards from './guards';
// routes
export const ROUTES: Routes = [
{
path: '',
canActivate: [fromGuards.SomeGuard],
component: fromContainers.SomeComponent,
},
Provides us with Entity State adapter for managing record collections in a performant and type-safe way.
In examples (1, 2) hereinabove we managed collections by ourselves. In examples for this section we'll try to change those examples to use @ngrx/entity.
Allows to write reducer operations quickly and to generate selectors automatically.
Takes an object for configuration with properties:
- selectId - a method for selecting the primary id for the collection;
- sortComparer - a compare function used to sort the collection in case the collection needs to be sorted before being displayed.
// customers.reducer.ts
export const customerAdapter: EntityAdapter<Customer> = createEntityAdapter<Customer>();
Predefined generic interface for a given entity collection.
interface EntityState<V> {
// An array of all the primary ids in the collection.
ids: string[] | number[];
// A dictionary of entities in the collection indexed by the primary id.
entities: { [id: string | id: number]: V };
}
// customers.reducer.ts
export interface CustomerState extends EntityState<Customer> {
// Additional entities state properties.
loaded: boolean;
loading: boolean;
}
Returns the initialState for entity state based on the provided type.
// customers.reducer.ts
export const initialState: CustomerState = customerAdapter.getInitialState({
// Additional entity state properties.
loaded: false,
loading: false,
});
// customers.reducer.ts
export function reducer(state = initialState, action: CustomersAction): CustomerState {
switch (action.type) {
case CustomersActionTypes.LOAD_CUSTOMERS_SUCCESS: {
return customerAdapter.addMany(action.payload.customers, {
...state,
loaded: true,
loading: false,
});
}
case CustomersActionTypes.CREATE_CUSTOMER_SUCCESS: {
return customerAdapter.addOne(action.payload.customer, state);
}
case CustomersActionTypes.UPDATE_CUSTOMER_SUCCESS: {
return customerAdapter.updateOne(action.payload.customer, state);
}
case CustomersActionTypes.REMOVE_CUSTOMER_SUCCESS: {
return customerAdapter.removeOne(action.payload.customer.id, state);
}
default: {
return state;
}
}
}
The getSelectors
method returned by the entity adapter provides functions for selecting information from the entity.
Takes a selector function as argument to select the piece of state for a defined entity.
// customers.selecttor.ts
import { createSelector } from '@ngrx/store';
import * as fromFeature from '../reducers';
import * as fromCustomers from '../reducers/customers.reducer';
export const selectCustomersState = createSelector(fromFeature.selectUsersState, (state: fromFeature.UsersState) => state.customers);
export const {
selectIds: selectCustomersIds,
selectEntities: selectCustomersEntities,
selectAll: selectAllCustomers,
selectTotal: selectCustomersTotal,
} = fromCustomers.customerAdapter.getSelectors(selectCustomersState);
Introduces time-traveling debugging.
// app.module.ts
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
@NgModule({
imports: [
environment.production ? [] : StoreDevtoolsModule.instrument(),
],
})
export class AppModule { }
- NGRX Course: Store + Effects by @toddmotto.
- Comprehensive Introduction to @ngrx/store by @btroncone.
- Documentation for @ngrx/platform by contributors.
- From Inactive to Reactive with ngrx by @brandonroberts and @MikeRyanDev.
- Reactive Angular2 with ngRx by @robwormald.
- Introducing @ngrx/entity by @MikeRyanDev.
- ($) Build Redux Style Applications with Angular, RxJS, and ngrx/store by @johnlindquist.