Share
Embed
Introducing state management into an application with NgRx
In this workshop, we will gradually introduce state management with NgRx into an existing application. First, we demonstrate how this can be solved using the classic NgRx store and how it ensures clear and efficient management of the application state.
We then modernize the application by introducing the NgRx Signal Store, which offers new approaches to management and reactivity in state management.
Fabian Gosebrink
March 19, 2025
Transcript
47 slides
-
State Management with NgRx Master centralized state management with Classic
NgRx, using actions, reducers, effects, and selectors for scalable applications! -
App Component 1 Component 2 Component 3 Service 1 Service
2 Service 3 NgRx Classic Motivation -
App Component 1 Component 2 Component 3 Service 1 Service
2 Service 3 NgRx Classic Motivation -
App Component 1 Component 2 Component 3 Service 1 Service
2 Service 3 NgRx Classic Motivation -
App Component 1 Component 2 Component 3 State Service 1
Service 3 Service 2 Reducer Effects NgRx Classic -
One "source of truth" Easy to save Easy to debug
Undo/Redo Immutables and Observables, Signals Performance Reading: directly Writing: Via Reducer NgRx Classic State -
State UI Actions Reducer Store defines triggers send to updates
contains NgRx Classic -
export interface TodoState { items: Todo[]; } export const initialState:
TodoState = { items: [], }; NgRx Classic -
export const TodosActions = createActionGroup({ source: '[Todo]', events: { 'Load
All Todos': emptyProps(), 'Load All Todos Finished': props<{ products: Product[] }>(), 'Load All Todos Failure': props<{ error: HttpErrorResponse }>(), }, }); 1 2 3 4 5 6 7 8 NgRx Classic -
export const TodosActions = createActionGroup({ source: '[Todo]', events: { 'Load
All Todos': emptyProps(), 'Load All Todos Finished': props<{ products: Product[] }>(), 'Load All Todos Failure': props<{ error: HttpErrorResponse }>(), }, }); 1 2 3 4 5 6 7 8 'Load All Todos': emptyProps(), export const TodosActions = createActionGroup({ 1 source: '[Todo]', 2 events: { 3 4 'Load All Todos Finished': props<{ products: Product[] }>(), 5 'Load All Todos Failure': props<{ error: HttpErrorResponse }>(), 6 }, 7 }); 8 NgRx Classic -
export const TodosActions = createActionGroup({ source: '[Todo]', events: { 'Load
All Todos': emptyProps(), 'Load All Todos Finished': props<{ products: Product[] }>(), 'Load All Todos Failure': props<{ error: HttpErrorResponse }>(), }, }); 1 2 3 4 5 6 7 8 'Load All Todos': emptyProps(), export const TodosActions = createActionGroup({ 1 source: '[Todo]', 2 events: { 3 4 'Load All Todos Finished': props<{ products: Product[] }>(), 5 'Load All Todos Failure': props<{ error: HttpErrorResponse }>(), 6 }, 7 }); 8 'Load All Todos Finished': props<{ products: Product[] }>(), export const TodosActions = createActionGroup({ 1 source: '[Todo]', 2 events: { 3 'Load All Todos': emptyProps(), 4 5 'Load All Todos Failure': props<{ error: HttpErrorResponse }>(), 6 }, 7 }); 8 NgRx Classic -
export const TodosActions = createActionGroup({ source: '[Todo]', events: { 'Load
All Todos': emptyProps(), 'Load All Todos Finished': props<{ products: Product[] }>(), 'Load All Todos Failure': props<{ error: HttpErrorResponse }>(), }, }); 1 2 3 4 5 6 7 8 'Load All Todos': emptyProps(), export const TodosActions = createActionGroup({ 1 source: '[Todo]', 2 events: { 3 4 'Load All Todos Finished': props<{ products: Product[] }>(), 5 'Load All Todos Failure': props<{ error: HttpErrorResponse }>(), 6 }, 7 }); 8 'Load All Todos Finished': props<{ products: Product[] }>(), export const TodosActions = createActionGroup({ 1 source: '[Todo]', 2 events: { 3 'Load All Todos': emptyProps(), 4 5 'Load All Todos Failure': props<{ error: HttpErrorResponse }>(), 6 }, 7 }); 8 'Load All Todos Failure': props<{ error: HttpErrorResponse }>(), export const TodosActions = createActionGroup({ 1 source: '[Todo]', 2 events: { 3 'Load All Todos': emptyProps(), 4 'Load All Todos Finished': props<{ products: Product[] }>(), 5 6 }, 7 }); 8 NgRx Classic -
export const TodosActions = createActionGroup({ source: '[Todo]', events: { 'Load
All Todos': emptyProps(), 'Load All Todos Finished': props<{ products: Product[] }>(), 'Load All Todos Failure': props<{ error: HttpErrorResponse }>(), }, }); 1 2 3 4 5 6 7 8 'Load All Todos': emptyProps(), export const TodosActions = createActionGroup({ 1 source: '[Todo]', 2 events: { 3 4 'Load All Todos Finished': props<{ products: Product[] }>(), 5 'Load All Todos Failure': props<{ error: HttpErrorResponse }>(), 6 }, 7 }); 8 'Load All Todos Finished': props<{ products: Product[] }>(), export const TodosActions = createActionGroup({ 1 source: '[Todo]', 2 events: { 3 'Load All Todos': emptyProps(), 4 5 'Load All Todos Failure': props<{ error: HttpErrorResponse }>(), 6 }, 7 }); 8 'Load All Todos Failure': props<{ error: HttpErrorResponse }>(), export const TodosActions = createActionGroup({ 1 source: '[Todo]', 2 events: { 3 'Load All Todos': emptyProps(), 4 'Load All Todos Finished': props<{ products: Product[] }>(), 5 6 }, 7 }); 8 }, export const TodosActions = createActionGroup({ 1 source: '[Todo]', 2 events: { 3 'Load All Todos': emptyProps(), 4 'Load All Todos Finished': props<{ products: Product[] }>(), 5 'Load All Todos Failure': props<{ error: HttpErrorResponse }>(), 6 7 }); 8 NgRx Classic -
Reducer Function that executes Action Pure Function (stateless, etc.) Each
Reducer gets each Action Check whether Action is relevant This prevents cycles NgRx Classic -
export interface TodoState { items: Todo[]; } export const initialState:
TodoState = { items: [], }; const todoReducerInternal = createReducer( initialState, ... ); NgRx Classic - Reducer -
export interface TodoState { items: Todo[]; } export const initialState:
TodoState = { items: [], }; const todoReducer = createReducer( initialState, on(todoActions.loadAllTodosFinished, (state, { payload }) => ({ ...state, items: [...payload] })), ); NgRx Classic - Reducer -
export interface TodoState { items: Todo[]; } export const initialState:
TodoState = { items: [], }; const todoReducer = createReducer( initialState, on(todoActions.addTodoFinished, (state, { payload }) => ({ ...state, items: [...state.items, payload] })), on(todoActions.loadAllTodosFinished, (state, { payload }) => ({ ...state, items: [...payload] })), on(todoActions.loadSingleTodoFinished, (state, { payload }) => ({ ...state, selectedItem: payload })) ); NgRx Classic - Reducer -
export const routes: Routes = [ { path: 'todo', loadComponent:
..., providers: [ provideState("todos", todoReducer), ], } ]; 1 2 3 4 5 6 7 8 9 NgRx Classic - Feature Abstraction -
export const routes: Routes = [ { path: 'todo', loadComponent:
..., providers: [ provideState("todos", todoReducer), ], } ]; 1 2 3 4 5 6 7 8 9 provideState("todos", todoReducer), ], export const routes: Routes = [ 1 { 2 path: 'todo', 3 loadComponent: ..., 4 providers: [ 5 6 7 } 8 ]; 9 NgRx Classic - Feature Abstraction -
export const routes: Routes = [ { path: 'todo', loadComponent:
..., providers: [ provideState("todos", todoReducer), ], } ]; 1 2 3 4 5 6 7 8 9 NgRx Classic - Feature Abstraction { todos: { ... } } -
export const routes: Routes = [ { path: 'todo', loadComponent:
..., providers: [ provideState("todos", todoReducer), ], } ]; 1 2 3 4 5 6 7 8 9 provideState("todos", todoReducer), export const routes: Routes = [ 1 { 2 path: 'todo', 3 loadComponent: ..., 4 providers: [ 5 6 ], 7 } 8 ]; 9 NgRx Classic - Feature Abstraction { todos: { ... } } -
Always predictable !!! synchronous per default what about async? NgRx
Classic - Reducer -
export const updateTodo = createEffect( (actions$ = inject(Actions)) => {
return actions$.pipe( // ... ); }, { functional: true } ); NgRx Classic - Effects -
export const updateTodo = createEffect( ( actions$ = inject(Actions), todoService
= inject(TodoApiService) ) => { return actions$.pipe( ofType(TodoActions.updateTodo), exhaustMap((todo) => { return todoService.update(todo).pipe( map((updatedTodo) => DoggosActions.rateDoggoFinished({ updatedTodo }); ) ); }) ); }, { functional: true } ); NgRx Classic - Effects -
@Component({ ... }) export class ContentComponent implements OnInit { private
readonly store = inject(Store); items$: Observable<Todo[]>; ngOnInit() { ... } } NgRx Classic - Components -
@Component({ ... }) export class ContentComponent implements OnInit { private
readonly store = inject(Store); items$: Observable<Todo[]>; ngOnInit() { this.items$ = this.store.pipe( map((state) => state.todoFeature.items) ); } } NgRx Classic - Components -
Selectors Traverse the state tree Return pieces of state Memoized
NgRx Classic - Selectors -
export const featureName = 'todoFeature'; export const getTodoFeatureState = createFeatureSelector<TodoState>(featureName);
export const getAllItems = createSelector( getTodoFeatureState, (state: TodoState) => state.items ); NgRx Classic - Selectors -
export const featureName = 'todoFeature'; export const getTodoFeatureState = createFeatureSelector<TodoState>(featureName);
export const getAllItems = createSelector( getTodoFeatureState, (state: TodoState) => state.items ); export const getAllDoneItems = createSelector( getAllItems, (items: Todo[]) => tems.filter(x => x.done) ); export const getAllUndoneItems = createSelector( getAllItems, (items: Todo[]) => tems.filter(x => !x.done) ); NgRx Classic - Selectors -
@Component({ ... }) export class ContentComponent implements OnInit { private
readonly store = inject(Store); items$ = this.store.select(getAllUndoneItems); doneItems$ = this.store.select(getAllDoneItems); ngOnInit() { this.store.dispatch(todoActions.loadAllTodos()); } addTodo(item: string) { this.store.dispatch(todoActions.addTodo({ payload: item })); } markAsDone(item: Todo) { this.store.dispatch(todoActions.setAsDone({ payload: item })); } } NgRx Classic - Selectors -
@Component({ ... }) export class ContentComponent implements OnInit { private
readonly store = inject(Store); items = this.store.selectSignal(getAllUndoneItems); doneItems = this.store.selectSignal(getAllDoneItems); ngOnInit() { this.store.dispatch(todoActions.loadAllTodos()); } addTodo(item: string) { this.store.dispatch(todoActions.addTodo({ payload: item })); } markAsDone(item: Todo) { this.store.dispatch(todoActions.setAsDone({ payload: item })); } } NgRx Classic - Selectors