by Gerard Sans | @gerardsans
Spoken at 70 events in 23 countries
900
1.4K
Angular Academy
ngrx
Redux
Dan Abramov
source: blog
1
2
3
4
5
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
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
// function foo(x) { return x+1; }
let foo = x => x+1;
// pure function
foo(1); // 2
foo(1); // 2
let flag = false;
let foo = x => {
flag = !flag; // side effect
return flag ? x+1: 0;
}
// not pure function
foo(1); // 2
foo(1); // 0
source: blog
1
2
3
4
5
A
A
<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>
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);
1
2
3
4
5
@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 new todo
{
type: ADD_TODO,
id: 1,
text: "learn redux",
completed: false
}
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'
// }
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
// }
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
};
{
todos: [{
id: 1,
text: "learn redux",
completed: false
}],
currentFilter: 'SHOW_ALL'
}
// {
// todos: [], <-- we start with no todos
// currentFilter: 'SHOW_ALL'
// }
import { EffectsModule } from '@ngrx/effects';
import { todoEffects } from './todoEffects';
@NgModule({
imports: [
StoreModule.forRoot(rootReducer),
EffectsModule.forRoot([todoEffects]),
]
})
export class AppModule {}
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
}))
)
}
// 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
}))
)
@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 }
})
)
)
}
> 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
// 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;
// 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;
}
}
// 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;
// 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>;
}
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
};
// 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')
);
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 {}
@MikeRyanDev
@robwormald
@brandontroberts
@toddmotto