Advanced State
Management using new ngrx v5
by Gerard Sans | @gerardsans
Google Developer Expert
Google Developer Expert
International Speaker
Spoken at 70 events in 23 countries
Blogger
Blogger
Community Leader
900
1.4K
Trainer
Master of Ceremonies
Master of Ceremonies
Angular Academy
Developer
Experience
extension.remotedev.io
Features
- Save/Restore State
- Live Debugging
- Time travel
- Dispatch Actions
Redux
State Management
ngrx
Redux
Dan Abramov
Scattered State
Redux Principles
- Unidirectional data flow
- Single Store
- No side-effects
Unidirectional data flow
source: blog
1
2
3
4
5
Single State
Immutable
- Helps tracking changes by reference
- Improved Performance
- Enforce by convention or using a library. Eg: Immutable.js
Immutable by Convention
- New array using Array Methods
- map, filter, slice, concat
- Spread operator (ES6) [...arr]
- New object using Object.assign (ES6)
Example by Convention
let selectedUsers = [1, 2, 3];
let user = { id: 4, username: 'Spiderman'};
let newSelection = selectedUsers.concat([4, 5, 6]); // [1, 2, 3, 4, 5, 6];
let newUser = Object.assign({}, user, { admin: true });
console.log(newUser.admin) // true
// NOT VALID
selectedUsers.push(4, 5, 6); // reference to 'selectedUsers' still the same
user.admin = true; // reference to 'user' still the same
Immutable by using a library
- Recommended for large teams
- Library enforces right usage
- Peace of mind
Using Immutable.js
let selectedUsers = Immutable.List([1, 2, 3]);
let user = Immutable.Map({ id: 4, username: 'Spiderman'}):
let newSelection = selectedUsers.push(4, 5, 6); // [1, 2, 3, 4, 5, 6];
let newUser = user.set('admin', true);
newUser.get('admin') // true
Reducers
- Reducers create new states in response to Actions applied to the current State
- Reducers are pure functions
- Don't produce side-effects
- Composable
Example: pure function
// function foo(x) { return x+1; }
let foo = x => x+1;
// pure function
foo(1); // 2
foo(1); // 2
Example: side-effect
let flag = false;
let foo = x => {
flag = !flag; // side effect
return flag ? x+1: 0;
}
// not pure function
foo(1); // 2
foo(1); // 0
Middlewares
- Sit between Actions and Reducers
- Used for logging, storage and asynchronous operations
- Composable
Performance
Change Detection
Change Detection
ngrx
v5
What's new in v5?
- @ngrx/schematics (@angular-devkit)
- 'Pipeable' select operator (lettable)
- Support custom createSelector
- RxJS 5.5
- UpsertOne/Many
-
Suite including
- @ngrx/store*
- @ngrx/effects
- @ngrx/router-store
- @ngrx/store-devtools*
- @ngrx/entity*
- @ngrx/schematics*
- Re-implementation of Redux on top Angular and RxJS 5.
-
Colocation Support
- StoreModule.forRoot()
- StoreModule.forFeature()
- Payload Type checking
- Asynchronous Actions Middleware
- Executes side-effects
-
Colocation Support
- EffectsModule.forRoot()
- EffectsModule.forFeature()
Asynchronous Actions
source: blog
1
2
3
4
5
A
A
Solution Architecture
Components Tree
Components Tree
<app>
<add-todo>
<input><button>Add todo</button>
</add-todo>
<todo-list>
<ul>
<todo id="0" completed="false"><li>buy milk</li></todo>
</ul>
</todo-list>
<filters>
Show: <filter-link><a>All</a><filter-link> ...
</filters>
</app>
Setup
import { App } from './app';
import { StoreModule } from "@ngrx/store";
import { rootReducer } from './rootReducer';
@NgModule({
imports: [
BrowserModule,
StoreModule.forRoot(rootReducer)
],
declarations: [ App ],
bootstrap: [ App ]
})
export class AppModule {}
platformBrowserDynamic().bootstrapModule(AppModule);
Adding a new Todo
- Component subscribes
- Component dispatches ADD_TODO action
- Store executes rootReducer
- Store notifies Component
- View updates
1
2
3
4
5
Subscribing to the Store
@Component({
template:
`<todo *ngFor="let todo of todos | async">{{todo.text}}</todo>`
})
export class App implements OnDestroy {
public todos: Observable<Todo>;
constructor(
private _store: Store<TodosState>
) {
this.todos = _store.let(
state$ => state$.select(s => s.todos);
);
}
}
ADD_TODO Action
// add new todo
{
type: ADD_TODO,
id: 1,
text: "learn redux",
completed: false
}
todos Reducer
const initialState = [];
const todos = (state = initialState, action:Action) => {
switch (action.type) {
case TodoActions.ADD_TODO:
return state.concat({
id: action.id,
text: action.text,
completed: action.completed });
default: return state;
}
}
// {
// todos: [], <-- todos reducer will mutate this key
// currentFilter: 'SHOW_ALL'
// }
currentFilter Reducer
const currentFilter = (state = 'SHOW_ALL', action: Action) => {
switch (action.type) {
case TodoActions.SET_CURRENT_FILTER:
return action.filter
default: return state;
}
}
// {
// todos: [],
// currentFilter: 'SHOW_ALL' <-- filter reducer will mutate this key
// }
rootReducer
import { ActionReducerMap } from '@ngrx/store';
interface Todo {
id: number,
text: string,
completed: boolean
}
export interface TodosState {
todos: Array<Todo>;
currentFilter: string;
}
export const rootReducer: ActionReducerMap<TodosState> = {
todos: todos,
currentFilter: currentFilter
};
New State
{
todos: [{
id: 1,
text: "learn redux",
completed: false
}],
currentFilter: 'SHOW_ALL'
}
// {
// todos: [], <-- we start with no todos
// currentFilter: 'SHOW_ALL'
// }
ngrx/effects
Setup
import { EffectsModule } from '@ngrx/effects';
import { todoEffects } from './todoEffects';
@NgModule({
imports: [
StoreModule.forRoot(rootReducer),
EffectsModule.forRoot([todoEffects]),
]
})
export class AppModule {}
Effects Service
import { Actions, Effect, ofType } from '@ngrx/effects';
import { TodoActions, ADD_TODO_EFFECT } from './todoActions';
@Injectable()
export class todoEffects {
constructor(
private actions$ : Actions,
private actions : TodoActions ) { }
@Effect()
addTodoAsync$ = this.actions$
.ofType(ADD_TODO_EFFECT)
.mergeMap(action => this.http.post('/todo', action.text)
.map(response => ({
type: 'ADD_TODO_SUCCESS',
text: text
}))
)
}
Add new todo async
// Dispatch
private addTodoAsync(input) {
this._store.dispatch({
type: ADD_TODO_EFFECT
text: input.value
});
input.value = '';
}
// Middleware
@Effect()
addTodoAsync$ = this.actions$
.ofType(ADD_TODO_EFFECT)
.mergeMap(action => this.http.post('/todo', action.text)
.map(response => ({
type: 'ADD_TODO_SUCCESS',
text: text
}))
)
Error Handling
@Injectable()
export class todoEffects {
@Effect()
addTodoAsync$ = this.actions$
.ofType(ADD_TODO_EFFECT)
.mergeMap(action => this.http.post('/todo', action.text)
.map(response => ({
type: 'ADD_TODO_SUCCESS',
text: text
}))
.catch(error => Observable.of({
type: 'ADD_TODO_FAILED',
error: { message: error }
})
)
)
}
ngrx/entity
- Library to manage Lists
-
High performance
- EntityState<T>
- Ids lookup and Entities Map
-
Reducer Helper (EntityAdapter)
- getInitialState
- Add/update/remove: All, One, Many
- getSelectors: Ids, Entities, All, Total
- Scaffolding templates for ngrx
-
Provides commands for
- Setting initial Store and Effects
- Actions, Reducers, Entities, Features
- Containers + Store injected
- Automates creation + registration
ngrx/schematics commands
> ng new ngrx-entity-todo
> npm install @ngrx/schematics --save-dev
> npm install @ngrx/{store, effects, entity, store-devtools} --save
> ng generate store State --root --module app.module.ts --collection @ngrx/schematics
> ng generate effect App --root --module app.module.ts --collection @ngrx/schematics
> ng generate entity Todo -m app.module.ts --collection @ngrx/schematics
Filter Actions
// src/app/reducers/currentFilter/currentFilter.actions.ts
import { Action } from '@ngrx/store';
export enum CurrentFilterActionTypes {
SetCurrentFilter = '[Filter] Set current filter'
}
export class SetCurrentFilter implements Action {
readonly type = CurrentFilterActionTypes.SetCurrentFilter;
constructor(public payload: { filter: string }) {}
}
export type CurrentFilterActions = SetCurrentFilter;
Filter Reducer
// src/app/reducers/currentFilter/currentFilter.reducer.ts
export const initialState: string = 'SHOW_ALL';
export function reducer(
state = initialState,
action: CurrentFilterActions): string
{
switch (action.type) {
case CurrentFilterActionTypes.SetCurrentFilter:
return action.payload.filter
default:
return state;
}
}
Todos Actions
// src/app/reducers/todo/todo.reducer.ts
export enum TodoActionTypes {
AddTodo = '[Todo] Add Todo', UpdateTodo = '[Todo] Update Todo', ...
}
let currentId = 1;
export class AddTodo implements Action {
readonly type = TodoActionTypes.AddTodo;
constructor(public payload: { todo: Todo }) {
payload.todo.id = currentId++;
}
}
export type TodoActions = LoadTodos | AddTodo | UpsertTodo | AddTodos
| UpsertTodos | UpdateTodo | UpdateTodos | DeleteTodo | DeleteTodos | ClearTodos;
Todos Model
// src/app/reducers/todo/todo.model.ts
import { EntityState } from '@ngrx/entity';
export interface Todo {
id: number;
completed: boolean;
text: string;
}
export interface Todos {
todos: EntityState<Todo>;
}
Composing Reducers
import * as todos from './todo/todo.reducer';
import * as currentFilter from './currentFilter/currentFilter.reducer';
export interface Filter {
currentFilter: string;
}
export interface Todos {
todos: EntityState<Todo>;
}
export interface TodosState extends Todos, Filter { }
export const reducers: ActionReducerMap<TodosState> = {
todos: todos.reducer,
currentFilter: currentFilter.reducer
};
New Pipeable Selectors
// ngrx v4
// export const getTodos = state$ => state$.select(s => s.todos);
// export const getCurrentFilter = state$ => state$.select('currentFilter');
export const getTodos = state$ => state$.pipe(
select('todos'),
map(todoEntity.selectAll)
);
export const getCurrentFilter = state$ => state$.pipe(
select('currentFilter')
);
AppModule Setup
import { StoreModule } from "@ngrx/store";
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { AppComponent } from './app.component';
import { reducers, metaReducers } from './reducers';
@NgModule({
imports: [
BrowserModule,
StoreModule.forRoot(reducers, { metaReducers }),
!environment.production ? StoreDevtoolsModule.instrument() : [],
],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule {}
Why use Redux?
Main Benefits
- Simplified Development
- Avoids complex dependencies
- Great Performance
- Developer Experience
More
@MikeRyanDev
@robwormald
@brandontroberts
Rob Wormald
Mike Ryan
Brandon Roberts
@toddmotto
Todd Motto
Advanced State Management using new ngrx v5
By Gerard Sans
Advanced State Management using new ngrx v5
In this talk we are going to use the latest version of Angular and ngrx (v5) to learn advanced state management. We will introduce Redux principles before moving into ngrx/store, ngrx/effects for asynchronous actions, ngrx/entity and ngrx/schematics. ngrx/store uses an implementation inspired by Redux but using Observables from RxJS 5.5. We will also cover best practices and how to implement error handling.
- 3,389