Angular redux (state management)
action/effects/reducer/store(state,selector)
offcial documentation :https://ngrx.io/guide/store/selectors
version (https://ngrx.io/docs):
Version 14 has the minimum version requirements:
Angular version 14.x
Angular CLI version 14.x
TypeScript version 4.6.x
RxJS version ^6.5.3 || ^7.5.0
Version 12 has the minimum version requirements:
Angular version 12.x
Angular CLI version 12.x
TypeScript version 4.2.x
RxJS version 6.5.x
V7 has the minimum version requirements:
Angular version 7
TypeScript version 3.1.x
RxJS version 6.x
actions
https://ngrx.io/guide/store/actions
Action Interface
interface Action {
type: string;
}
login-page.actions.ts
import { createAction, props } from '@ngrx/store';
export const login = createAction(
'[Login Page] Login',
props<{ username: string; password: string }>()
);
login-page.component.ts
onSubmit(username: string, password: string) {
store.dispatch(login({ username: username, password: password }));
}
https://codeburst.io/angular-10-ngrx-store-by-example-333cbf16862c
* Action can just be dispatched as an object
// src/app/product/product.component.ts
import { Product } from './product.model';
import { AppState } from './../app.state';
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
@Component({
selector: 'app-product',
templateUrl: './product.component.html',
styleUrls: ['./product.component.css']
})
export class ProductComponent implements OnInit {
products: Observable<Product[]>;
constructor(private store: Store<AppState>) {
this.products = this.store.select(state => state.product);
}
addProduct(name, price) {
this.store.dispatch({
type: 'ADD_PRODUCT',
payload: <Product> {
name: name,
price: price
}
});
}
ngOnInit() {
}
}
reducers
https://ngrx.io/guide/store/reducers
*handles action, save the state in store
scoreboard-page.actions.ts
import { createAction, props } from '@ngrx/store';
export const homeScore = createAction('[Scoreboard Page] Home Score');
export const awayScore = createAction('[Scoreboard Page] Away Score');
export const resetScore = createAction('[Scoreboard Page] Score Reset');
export const setScores = createAction('[Scoreboard Page] Set Scores', props<{game: Game}>());
scoreboard.reducer.ts
import { Action, createReducer, on } from '@ngrx/store';
import * as ScoreboardPageActions from '../actions/scoreboard-page.actions';
export interface State {
home: number;
away: number;
}
export const initialState: State = {
home: 0,
away: 0,
};
export 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 }))
);
!!! Register reducer, reducer is registered in store
app.module.ts
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { scoreboardReducer } from './reducers/scoreboard.reducer';
@NgModule({
imports: [
StoreModule.forRoot({ game: scoreboardReducer })
],
})
export class AppModule {}
--------------------
register reducer as feature :
!!! Register reducer, reducer is registered in store
app.module.ts
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
@NgModule({
imports: [
StoreModule.forRoot({})
],
})
export class AppModule {}
scoreboard.reducer.ts
export const scoreboardFeatureKey = 'game';
scoreboard.module.ts
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { scoreboardFeatureKey, scoreboardReducer } from './reducers/scoreboard.reducer';
@NgModule({
imports: [
StoreModule.forFeature(scoreboardFeatureKey, scoreboardReducer)
],
})
export class ScoreboardModule {}
stores(state, selectors)
https://codeburst.io/angular-10-ngrx-store-by-example-333cbf16862c
1) define state :
src/app/app.state.ts
// src/app/app.state.ts
import { Product } from './product/product.model';
export interface AppState {
readonly product: Product[];
}
2) Import state and store in component
(!!! reducer is registered in store at app module)
// src/app/product/product.component.ts
import { Product } from './product.model';
import { AppState } from './../app.state';
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
@Component({
selector: 'app-product',
templateUrl: './product.component.html',
styleUrls: ['./product.component.css']
})
export class ProductComponent implements OnInit {
products: Observable<Product[]>;
constructor(private store: Store<AppState>) {
this.products = this.store.select(state => state.product);
}
selectors:
https://ngrx.io/guide/store/selectors
(to select some piece of state instead of returning all state)
index.ts
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 selectVisibleBooks = createSelector(
selectUser,
selectAllBooks,
(selectedUser: User, allBooks: Book[]) => {
if (selectedUser && allBooks) {
return allBooks.filter((book: Book) => book.userId === selectedUser.id);
} else {
return allBooks;
}
}
);
addtional selector example (https://www.codemag.com/article/1811061/Angular-and-the-Store)
export const getAllDevelopers =
createSelector(getState, (state): Developer[] => {
return state && state.developers;
}
);
this.store.select<Developer[]>(getAllDevelopers).subscribe(
developers => console.log(developers)
);
effects
https://v7.ngrx.io/guide/effects
!!! effects just pipe on action, do an api call, map the result into new action to handle by reducer !!!!
// Without effects
movies-page.component.ts
@Component({
template: `
<li *ngFor="let movie of movies">
{{ movie.name }}
</li>
`
})
export class MoviesPageComponent {
movies: Movie[];
constructor(private movieService: MoviesService) {}
ngOnInit() {
this.movieService.getAll().subscribe(movies => this.movies = movies);
}
}
movies.service.ts
@Injectable({
providedIn: 'root'
})
export class MoviesService {
constructor (private http: HttpClient) {}
getAll() {
return this.http.get('/movies');
}
}
// with effects
movies-page.component.ts
@Component({
template: `
<div *ngFor="let movie of movies$ | async">
{{ movie.name }}
</div>
`
})
export class MoviesPageComponent {
movies$: Observable = this.store.select(state => state.movies);
constructor(private store: Store<{ movies: Movie[] >}) {}
ngOnInit() {
this.store.dispatch({ type: '[Movies Page] Load Movies' });
}
}
movie.effects.ts
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
@Injectable()
export class MovieEffects {
@Effect()
loadMovies$ = this.actions$
.pipe(
ofType('[Movies Page] Load Movies'),
mergeMap(() => this.moviesService.getAll()
.pipe(
map(movies => ({ type: '[Movies API] Movies Loaded Success', payload: movies })),
catchError(() => of({ type: '[Movies API] Movies Loaded Error' }))
))
)
);
constructor(
private actions$: Actions,
private moviesService: MoviesService
) {}
}
Register effects at root
app.module.ts
import { EffectsModule } from '@ngrx/effects';
import { MovieEffects } from './effects/movie.effects';
@NgModule({
imports: [
EffectsModule.forRoot([MovieEffects])
],
})
export class AppModule {}