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.