Skip to main content

Lab 18: NgRx β€” Redux State Management

πŸ“– Resources​

πŸš€ Starter Code​

album-wholesale-v16

In this lab you will add centralized state management to the album wholesale app using NgRx classic (@ngrx/store + @ngrx/effects). You will manage the album list and loading state through a Redux store, replace direct service calls in components with store dispatches and selectors, and debug with Redux DevTools.

πŸ“ Instructions​

Step 1: Install NgRx​

npm install @ngrx/store @ngrx/effects @ngrx/store-devtools

Step 2: Define State, Actions and Reducer​

Create src/app/store/album/album.actions.ts:

import { createAction, props } from '@ngrx/store';
import { Album } from '../../model/album.model';

export const loadAlbums = createAction('[Album] Load Albums');
export const loadAlbumsSuccess = createAction(
'[Album] Load Albums Success',
props<{ albums: Album[] }>()
);
export const loadAlbumsFailure = createAction(
'[Album] Load Albums Failure',
props<{ error: string }>()
);

Create src/app/store/album/album.reducer.ts:

import { createReducer, on } from '@ngrx/store';
import { Album } from '../../model/album.model';
import { loadAlbums, loadAlbumsSuccess, loadAlbumsFailure } from './album.actions';

export interface AlbumState {
albums: Album[];
loading: boolean;
error: string | null;
}

const initialState: AlbumState = {
albums: [],
loading: false,
error: null,
};

export const albumReducer = createReducer(
initialState,
on(loadAlbums, state => ({ ...state, loading: true })),
on(loadAlbumsSuccess, (state, { albums }) =>
({ ...state, albums, loading: false })),
on(loadAlbumsFailure, (state, { error }) =>
({ ...state, error, loading: false }))
);

Step 3: Create a Selector​

Create src/app/store/album/album.selectors.ts:

import { createFeatureSelector, createSelector } from '@ngrx/store';
import { AlbumState } from './album.reducer';

export const selectAlbumState =
createFeatureSelector<AlbumState>('albums');

export const selectAllAlbums = createSelector(
selectAlbumState,
state => state.albums
);

export const selectAlbumsLoading = createSelector(
selectAlbumState,
state => state.loading
);

Step 4: Create an Effect​

Create src/app/store/album/album.effects.ts:

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { switchMap, map, catchError, of } from 'rxjs';
import { AlbumService } from '../../services/album.service';
import { loadAlbums, loadAlbumsSuccess, loadAlbumsFailure } from './album.actions';

@Injectable()
export class AlbumEffects {
loadAlbums$ = createEffect(() =>
this.actions$.pipe(
ofType(loadAlbums),
switchMap(() =>
this.albumService.findAll().pipe(
map(albums => loadAlbumsSuccess({ albums })),
catchError(error =>
of(loadAlbumsFailure({ error: error.message }))
)
)
)
)
);

constructor(
private actions$: Actions,
private albumService: AlbumService
) {}
}

Step 5: Wire the Store in AppModule​

Open src/app/app.module.ts and add the NgRx modules to the imports array:

import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { albumReducer } from './store/album/album.reducer';
import { AlbumEffects } from './store/album/album.effects';

@NgModule({
declarations: [ /* ... */ ],
imports: [
BrowserModule,
HttpClientModule,
// ... other existing imports
StoreModule.forRoot({ albums: albumReducer }),
EffectsModule.forRoot([AlbumEffects]),
StoreDevtoolsModule.instrument({ maxAge: 25 }),
],
bootstrap: [AppComponent]
})
export class AppModule {}

Step 6: Use the Store in a Component​

Replace the direct service call in AlbumListComponent:

import { Store } from '@ngrx/store';
import { loadAlbums } from '../../store/album/album.actions';
import { selectAllAlbums, selectAlbumsLoading } from '../../store/album/album.selectors';

export class AlbumListComponent implements OnInit {
private store = inject(Store);

albums$ = this.store.select(selectAllAlbums);
loading$ = this.store.select(selectAlbumsLoading);

ngOnInit() {
this.store.dispatch(loadAlbums());
}
}
<mat-progress-bar *ngIf="loading$ | async" mode="indeterminate"></mat-progress-bar>

<app-album-card
*ngFor="let album of albums$ | async"
[album]="album">
</app-album-card>

Step 7: Verify with Redux DevTools​

Install the Redux DevTools Chrome Extension and inspect:

  • Each dispatched action
  • State before and after each action
  • Time-travel debugging

Compare your result with the album-wholesale-v16-ngrx reference folder.