As a Front-end Lead Developer, I want to share my knowledge about front-end technologies and best practices. Here are a few lessons to boost your Angular expertise !
- Angular CLI v19+
- Node.js v22.12+
- Basic knowledge of Angular components and services
Before diving into the exercises, it's crucial to understand what NgRx is, when to use it, and how to implement it.
π Read this article to learn first
The goal of the following exercises is to enhance a simple notebook app by replacing the current service-based state management with NgRx. The goal is to persist the notes even after a page refresh.
π― Try the final app: Live Demo
π» Check the final code: ngrx-notes-final
Follow these steps to get started:
- Clone the repository:
git clone https://github.com/BenjaminCanape/AdvancedAngularLessons.git
- Checkout the start branch: ngrx-notes-start
- Install dependencies:
npm i --legacy-peer-deps
- Run the application:
(HMR is disabled to avoid page flickering issues.)
ng serve --no-hmr
-
Install NgRx packages:
npm install @ngrx/store @ngrx/effects @ngrx/store-devtools @ngrx/schematics
-
Set up the store:
- Create a
store
folder insidesrc/app
- Inside the
store
folder, create:app.state.ts
- A
note
folder containingnote.reducer.ts
- Create a
-
Define the note state inside
note.reducer.ts
:import { createReducer } from '@ngrx/store'; export interface NoteState {} const initialState: NoteState = {}; export const noteReducer = createReducer(initialState);
-
Configure the global state in
app.state.ts
:import { NoteState } from './note/note.reducer'; export interface AppState { notes: NoteState; }
-
Provide the store in
app.config.ts
:const reducers: ActionReducerMap<AppState> = { notes: noteReducer }; export const appConfig: ApplicationConfig = { providers: [ provideStore(reducers); ] }
π Congratulations, your store is now configured!
- Forgetting to import
provideStore
inapp.config.ts
. - Incorrect reducer configuration leading to state issues.
- Q: Why do I need to create an
app.state.ts
? A: It provides a centralized place to manage the global application state.
π Branch: ngrx-notes-exercise-2
- Move
note.model.ts
tosrc/app/store/note
- Modify
note.reducer.ts
to includenotes
andselectedId
properties in the state. - Define an appropriate initial state.
- Storing unnecessary data in the state.
- Not initializing properties correctly.
- Q: What should be included in the state? A: Store only necessary properties in the state.
π Branch: ngrx-notes-exercise-3
- Create actions for adding, updating, deleting and selecting notes.
- Modify the reducer to handle the new actions (some actions will be used later in effects, not here).
Example:
note.actions.ts
import { createActionGroup, props } from '@ngrx/store';
export const noteActions = createActionGroup({
source: 'Note',
events: {
Add: props<{ note: NoteData }>(),
'Add Success': props<{ note: Note }>(),
'Add Failure': props<{ error: string }>(),
},
});
note.reducer.ts
export const noteReducer = createReducer(
initialState,
on(noteActions.addSuccess, (state, { note }) => ({
...state,
notes: [note, ...state.notes],
})),
);
- Incorrect use of immutable state updates.
- Q: Why do I need separate success and failure actions? A: It helps in handling API responses more efficiently.
π Branch: ngrx-notes-exercise-4
- Implement side effects using
createEffect
for handling asynchronous operations for adding, updating and deleting notes. - Call the API service inside the effect and dispatch success/failure actions.
- Display user information (a snackbar for instance) when success or failure actions are dispatched
- Select a note when the note has been added successfully.
Example:
note.effect.ts
@Injectable()
export class NoteEffects {
private actions$ = inject(Actions);
private noteService = inject(NoteService);
addNote$ = createEffect(() =>
this.actions$.pipe(
ofType(noteActions.add),
switchMap(({ note }) =>
this.noteService.add(note).pipe(
map(note => noteActions.addSuccess({ note })),
catchError(error => of(noteActions.addFailure({ error })))
)
)
)
);
addNoteSuccess$ = createEffect(() =>
this.actions$.pipe(
ofType(noteActions.addSuccess),
switchMap(() => this.showSnackBar('notes.add_success'))
), { dispatch: false }
);
}
For information, you also need to modify the NoteService methods. We will clean this service soon, but in the first place, we will just edit the add, update and delete methods.
At the end of the add method, you can return of(noteObject)
, in the update one, you can return of(note)
and in the delete method, you can return of(true)
. It simulates the return of an api call as an Observable.
Be careful, your effects are not taken into account until your provide them in the application. So add this provideEffects([NoteEffects]),
in the ApplicationConfig object.
- Not providing effects in
ApplicationConfig
.
π Branch: ngrx-notes-exercise-5
- Create selectors to retrieve specific data from the state: all notes, the selected id and the selected note.
Example:
note.selector.ts
export const selectNoteState = createFeatureSelector<NoteState>('notes');
export const selectAllNotes = createSelector(selectNoteState, state => state.notes);
- Q: Can I select state from multiple features? A: Yes, by combining selectors from different feature states.
π Branch: ngrx-notes-exercise-6
- Implement a facade to encapsulate store logic and simplify component interaction.
- You want to expose the selectors and the methods to create, select, update and delete a note.
- Edit your components using the NoteService to now use the NoteFacade instead
- Clean the Note service from unnecessary signals and methods
- Test the application after that, it's using the store and it works.
Example:
note.facade.ts
@Injectable({
providedIn: 'root',
})
export class NoteFacade {
private store = inject(Store<NoteState>);
notes$ = this.store.select(selectAllNotes);
add(note: NoteData) {
return this.store.dispatch(noteActions.add({ note }));
}
}
π Branch: ngrx-notes-exercise-7
- Install and configure Redux DevTools for better state visualization and debugging with this documentation
- Then play with Redux devtools to see which actions are dispatched when you click on buttons or write notes.
π Branch: ngrx-notes-exercise-8
- Use
ngrx-store-localstorage
to persist state across browser refreshes. Install it and configure it with this documentation - When it's done, launch the application, add some notes, refresh the page and you will see that the store will be rehydrated with your previous data.
You have successfully completed the NgRx training! You are now equipped to build state-of-the-art Angular applications with NgRx.
π Need help? Check the final implementation: ngrx-notes-final