NgRx
Redux
Controller
User
Model
View
Updates
Manipulates
Sees
Uses
Model
Model
View
View
View
Action
Store
Dispatcher
View
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
The only way to change the state is to emit an action - special object that describes the way of changing the state.
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
const ADD_TODO = 'ADD_TODO'
store.dispatch(addTodo('Learn about actions'));
To specify how the state tree is transformed under specific actions, you need a pure function called reducer
function todoApp(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
}
default:
return state
}
}
// impure addToCart mutates existing cart
const addToCart = (cart, item, quantity) => {
cart.items.push({
item,
quantity
});
return cart;
};
// Pure addToCart() returns a new cart
// It does not mutate the original.
const addToCart = (cart, item, quantity) => {
const newCart = lodash.cloneDeep(cart);
newCart.items.push({
item,
quantity
});
return newCart;
};
@NgModule({
imports: [
// ...
StoreModule.forRoot({ featureKey: featureReducer }, { metaReducers }),
StoreRouterConnectingModule.forRoot(),
EffectsModule.forRoot([ AppEffects, FeatureEffects, /* ... */ ]),
!environment.production ? StoreDevtoolsModule.instrument() : [],
// ...
],
declarations: []
})
export class AppModule {}
@NgModule({
imports: [
// ...
StoreModule.forFeature({ otherFeatureKey: otherFeatureReducer }),
EffectsModule.forFeature([ FeatureEffects ]),
// ...
],
declarations: []
})
export class LazyLoadedModule {}
1. Root configuration
2. Feature configuration
import { createAction } from '@ngrx/store';
export const homeScore = createAction('[Scoreboard Page] Home Score');
export const awayScore = createAction('[Scoreboard Page] Away Score');
export const setScores = createAction(
'[Scoreboard Page] Set Scores',
props<{ home: number; away: number }>()
);
import { Store } from '@ngrx/store';
@Component({ /* ... */ })
export class ScoreboardComponent {
constructor(private store: Store<ScoreState>) {}
setScores(home, away) {
this.store.dispatch(setScores({ home, away }));
}
}
import { Action, createReducer, on } from '@ngrx/store';
import * as ScoreboardPageActions from '../actions/scoreboard.actions';
export interface ScoreState {
home: number;
away: number;
}
export const initialState: ScoreState = {
home: 0,
away: 0,
};
const scoreboardReducer = createReducer(
initialState,
on(ScoreboardPageActions.homeScore, state => ({ ...state, home: state.home + 1 })),
on(ScoreboardPageActions.awayScore, state => ({ ...state, away: state.away + 1 })),
on(ScoreboardPageActions.resetScore, state => ({ home: 0, away: 0 })),
on(ScoreboardPageActions.setScores, (state, { game }) => ({ home: game.home, away: game.away }))
);
export function reducer(state: ScoreState | undefined, action: ScoreboardAction) {
return scoreboardReducer(state, action);
}
import { createSelector } from '@ngrx/store';
export interface User {
id: number;
name: string;
}
export interface Book {
id: number;
userId: number;
name: string;
}
export interface AppState {
selectedUser: User;
allBooks: Book[];
}
export const selectUser = (state: AppState) => state.selectedUser;
export const selectAllBooks = (state: AppState) => state.allBooks;
export const getCount = createSelector(
selectAllBooks,
(allBooks: Book[]) => allBooks.length
);
export const selectVisibleBooks = createSelector(
selectUser,
selectAllBooks,
(selectedUser: User, allBooks: Book[]) => {
if (selectedUser && allBooks) {
return allBooks.filter((book: Book) => book.userId === selectedUser.id);
} else {
return allBooks;
}
}
);
ngOnInit() {
this.counter = this.store.select(getCount)
}
@Injectable()
export class MovieEffects {
loadMovies$ = createEffect(() => this.actions$.pipe(
ofType('[Movies Page] Load Movies'),
switchMap(() => this.moviesService.getAll()
.pipe(
map(movies => ({ type: '[Movies API] Movies Loaded Success', payload: movies })),
catchError((error) => ({ type: '[Movies API] Movies Loaded Failed', payload: error }))
))
)
);
constructor(
private actions$: Actions,
private moviesService: MoviesService
) {}
}
@Injectable()
export class MovieEffects {
@Effect()
loadMovies$ = this.actions$
.pipe(
ofType('[Movies Page] Load Movies'),
switchMap(() => this.moviesService.getAll()
.pipe(
map(movies => ({ type: '[Movies API] Movies Loaded Success', payload: movies })),
catchError((error) => ({ type: '[Movies API] Movies Loaded Failed', payload: error }))
))
)
);
constructor(
private actions$: Actions,
private moviesService: MoviesService
) {}
}