Angular
State Management
on Steroids using ngrx
slides.com/gerardsans
|
@gerardsans
Redux
Angular Data Layer
DATA CLIENTS
GraphQL
Firebase
ngrx
Redux
STATE MANAGEMENT
Dan Abramov
Main Principles
- Unidirectional data flow
- Single Immutable State
- New states are created without side-effects
Unidirectional data flow
source: blog
1
2
3
4
5
Single Immutable State
- 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)
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
Angular Data Layer
DATA CLIENTS
GraphQL
Firebase
ngrx
Redux
STATE MANAGEMENT
Rob Wormald
- Re-implementation of Redux on top Angular and RxJS 5
- ngrx suite: store, effects, router, db
- Asynchronous Actions Middleware
- Executes side-effects
- Action => Effect => Action (ok/error)
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.provideStore(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 { combineReducers } from '@ngrx/store';
const combinedReducer = combineReducers({
todos: todos,
currentFilter: currentFilter
});
export rootReducer = (state, action) => combinedReducer(state, action);
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: [
EffectsModule.run(todoEffects),
StoreModule.provideStore(rootReducer),
]
})
export class AppModule {}
Effects Service
import { Actions, Effect, toPayload, 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)
.map(toPayload)
.switchMap(text => this.http.post('/todo', text)
.map(response => ({
type: 'ADD_TODO_SUCCESS',
payload: text
}))
)
}
Add new todo async
// Dispatch
private addTodoAsync(input) {
this._store.dispatch({
type: ADD_TODO_EFFECT
payload: input.value
});
input.value = '';
}
// Middleware
@Effect()
addTodoAsync$ = this.actions$
.ofType(ADD_TODO_EFFECT)
.map(toPayload)
.switchMap(text => this.http.post('/todo', text)
.map(response => ({
type: 'ADD_TODO_SUCCESS',
payload: text
}))
)
Error Handling
@Injectable()
export class todoEffects {
@Effect()
addTodoAsync$ = this.actions$
.ofType(ADD_TODO_EFFECT)
.map(toPayload)
.switchMap(text => this.http.post('/todo', text)
.map(response => ({
type: 'ADD_TODO_SUCCESS',
payload: text
}))
.catch(error => Observable.of({
type: 'ADD_TODO_FAILED',
error: { message: error }
})
)
)
}
Redux Dev Tools
Features
- Save/Restore State
- Live Debugging
- Time travel
- Dispatch Actions
Why use Redux?
Main Benefits
- Simplified Development
- Avoids complex dependencies
- Great Performance
- Developer Experience
Thanks!
Angular State Management on Steroids Using ngrx (v4+)
By Gerard Sans
Angular State Management on Steroids Using ngrx (v4+)
In this talk, you will learn advanced state management using the ngrx suite including store, effects, router while demoing DevTools awesome time travel. ngrx/store is a Redux rewrite throwing Observables in the mix!
- 4,042