NgRx là hệ sinh thái quản lý state reactive trong Angular. Chương này giúp bạn nắm vững cách sử dụng các phần chính của NgRx để xây dựng ứng dụng có cấu trúc rõ ràng, dễ kiểm soát và mở rộng. Các kỹ thuật được trình bày:
- Tạo store đầu tiên với actions và reducers
- Debug state bằng Devtools
- Sử dụng selectors để lấy dữ liệu từ store
- Tích hợp effects để gọi API
- Dùng Component Store để quản lý state cục bộ cho component

✅ 1. Store đầu tiên với Actions & Reducers
- Mục tiêu: Tạo store cho tính năng thêm/xóa trái cây khỏi bucket.
- Các bước:
- Cài
@ngrx/store
(nếu cần). - Cấu hình
provideStore({})
trongapp.config.ts
. - Tạo file
bucket.actions.ts
dùngcreateActionGroup
- Tạo reducer trong
bucket.reducer.ts
- Cập nhật
app.config.ts
:provideStore({ bucket: bucketReducer })
- Trong component:
this.store.dispatch(BucketActions.addFruit({ fruit: newFruit }));
- Cài
//app.config.ts ... import { provideStore } from '@ngrx/store'; ... import { provideAnimations } from '@angular/platform-browser/animations'; import { provideStore } from '@ngrx/store'; export const appConfig: ApplicationConfig = { providers: [ ..., provideAnimations(), provideStore({ bucket: bucketReducer, }), ], }; //bucket.actions.ts ... export const BucketActions = createActionGroup({ source: 'Bucket', events: { 'Add Fruit': props<{ fruit: IFruit }>(), 'Remove Fruit': props<{ fruitId: number }>(), }, }); //bucket.reducer.ts ... import { createReducer, on } from '@ngrx/store'; import { IFruit } from '../interfaces/fruit.interface'; import { BucketActions } from './bucket.actions'; export const initialState: ReadonlyArray<IFruit> = []; ... export const bucketReducer = createReducer( initialState, on(BucketActions.addFruit, (_state, { fruit }) => { console.log({ fruit }); return [fruit, ..._state]; }), on(BucketActions.removeFruit, (_state, { fruitId }) => { console.log({ fruitId }); return _state.filter((fr) => fr.id === fruitId); }) );
Ghi nhớ: Reducer không được mutate state. Hành vi được thể hiện qua console log để debug dễ hơn.

🔧 2. Debug với NgRx Store Devtools
- Mục tiêu: Quan sát mọi action và state thay đổi qua thời gian.
- Cách thực hiện:
- Cài
@ngrx/store-devtools
- Thêm vào
app.config.ts
:provideStoreDevtools({ maxAge: 50 })
- Cài Redux Devtools Extension
- Khi chạy app, bật Chrome DevTools → tab Redux:
@@INIT
→ Khởi tạo storeAdd Fruit
→ thêm itemRemove Fruit
→ xóa item
- Cài

Highlight:
- Màu xanh = thêm state
- Màu đỏ + gạch ngang = xóa state
📃 3. Render dữ liệu bằng Selectors
- Vấn đề: Dù dùng NgRx để lưu state, UI vẫn dùng
BucketService
để render. - Giải pháp:
- Tạo file
bucket.selectors.ts
- Trong component:
$bucket = this.store.select(selectBucket);
- Xoá toàn bộ xử lý state trong BucketService
- Tạo file
import { createFeatureSelector } from '@ngrx/store'; import { IFruit } from '../interfaces/fruit.interface'; export const selectBucket = createFeatureSelector<ReadonlyArray<IFruit>>('bucket'); ... ... import { selectBucket } from '../store/bucket.selectors'; ... export class BucketComponent implements OnInit { ... store = inject(Store); $bucket: Observable<IFruit[]> = this.store.select( selectBucket); ngOnInit(): void { this.bucketService.loadItems(); } ... }
... import { Injectable } from '@angular/core'; import { IFruit } from '../interfaces/fruit.interface'; import { IBucketService } from '../interfaces/bucket-service'; ... export interface IBucketService { loadItems(): void; saveItems(fruit: IFruit[]): void; } ... @Injectable({ providedIn: 'root', }) export class BucketService implements IBucketService { storeKey = 'bucket_ngrx-selectors'; loadItems() { return JSON.parse(window.localStorage.getItem(this.storeKey) || '[]'); } saveItems(items: IFruit[]) { window.localStorage.setItem(this.storeKey, JSON.stringify(items)); } }
Ví dụ template:
<div *ngFor="let item of $bucket | async"> {{ item.name }} </div>
Ghi nhớ: Selectors giúp component “mỏng”, chỉ lo phần hiển thị, không quan tâm logic store.
🚀 4. Gọi API với NgRx Effects
- Mục tiêu: Kết nối backend để load/add/remove dữ liệu bucket.
- Cách làm:
- Cài đặt:
npm install –save @ngrx/effects - Tạo thêm các action
- Tạo
bucket.effects.ts
- Dispatch khi khởi tạo component:
ngOnInit() { this.store.dispatch(BucketActions.getBucket()); }
- Sửa reducer để lắng nghe success actions
- Cài đặt:
import { createActionGroup, props, emptyProps } from '@ngrx/store'; import { IFruit } from '../interfaces/fruit.interface'; export const BucketActions = createActionGroup({ source: 'Bucket', events: { 'Get Bucket': emptyProps(), 'Get Bucket Success': props<{ bucket: IFruit[] }>(), 'Get Bucket Failure': props<{ error: string }>(), 'Add Fruit': props<{ fruit: IFruit }>(), 'Add Fruit Success': props<{ fruit: IFruit }>(), 'Add Fruit Failure': props<{ error: string }>(), 'Remove Fruit': props<{ fruitId: number }>(), 'Remove Fruit Success': props<{ fruitId: number }>(), 'Remove Fruit Failure': props<{ error: string }>(), }, });
//bucket.effects.ts import { Injectable } from '@angular/core'; import { Actions, ofType, createEffect } from '@ngrx/effects'; import { of } from 'rxjs'; import { catchError, exhaustMap, map } from 'rxjs/operators'; import { BucketService } from '../bucket/bucket.service'; import { BucketActions } from './bucket.actions'; @Injectable() export class BucketEffects { getBucket$ = createEffect(() => this.actions$.pipe( ofType(BucketActions.getBucket), exhaustMap(() => this.bucketService.getBucket().pipe( map(({ bucket }) => BucketActions .getBucketSuccess({ bucket })), catchError((error) => of(BucketActions .getBucketFailure({ error }))) ) ) ) ); constructor( private actions$: Actions, private bucketService: BucketService ) {} } //app.config.ts ... import { provideEffects } from '@ngrx/effects'; ... import { BucketEffects } from './app/store/bucket.effects'; ... bootstrapApplication(AppComponent, { providers: [ ..., provideHttpClient(), provideEffects([BucketEffects]), ], }).catch((err) => console.error(err)); ... //bucket.reducer.ts export const initialState: ReadonlyArray<IFruit> = []; export const bucketReducer = createReducer( initialState, on(BucketActions.getBucketSuccess, (_state, { bucket }) => { return bucket; }), on(BucketActions.addFruitSuccess, (_state, { fruit }) => { console.log({ fruit }); return [fruit, ..._state]; }), on(BucketActions.removeFruitSuccess, (_state, { fruitId }) => { console.log({ fruitId }); return _state.filter((fr) => fr.id !== fruitId); }) );
Ghi nhớ: Dùng 3 actions cho mỗi call: trigger → success → failure. Giúp dễ debug và tách biệt luồng dữ liệu.
🔹 5. Component Store: quản lý state riêng cho component

- Tình huống: Chỉ cần state cho 1 component, không cần chia sẻ toàn ứng dụng.
- Cách triển khai:
- Kế thừa
ComponentStore<BucketState>
trong service:export interface BucketState { bucket: IFruit[]; }
- Dùng
this.setState()
để init từ localStorage. - Tạo selectors và updater
- Dùng trong component như sau:
this.store.addItem(newFruit);
- Kế thừa
... import { ComponentStore } from '@ngrx/component-store'; import { IFruit } from '../interfaces/fruit.interface'; export interface BucketState { bucket: IFruit[]; } ... export class BucketService extends ComponentStore<BucketState> { storeKey = 'bucket_ngrx-component-store'; readonly bucket$: Observable<IFruit[]> = this.select((state) => state.bucket); constructor() { super({ bucket: [] }); this.setState({ bucket: this.loadItems(), }); } readonly addItem = this.updater((state: BucketState, fruit: IFruit) => { const bucketUpdated = [fruit, ...state.bucket]; this.saveItems(bucketUpdated); return { bucket: bucketUpdated, }; }); readonly removeItem = this.updater((state: BucketState, fruitId: number) => { const bucketUpdated = state.bucket.filter((fr) => fr.id !== fruitId); this.saveItems(bucketUpdated); return { bucket: bucketUpdated, }; }); ... loadItems() {...} saveItems(items: IFruit[]) {...} }
... import { BucketService } from './bucket.service'; @Component({...}) export class BucketComponent { ... bucket: IFruit[] = []; //← remove store = inject(BucketService); //← add bucket$ = this.store.bucket$; //← add addSelectedFruitToBucket() { const newFruit: IFruit = { id: Date.now(), name: this.selectedFruit, }; this.store.addItem(newFruit); } deleteFromBucket(fruit: IFruit) { this.store.removeItem(fruit.id); } }
<div class="fruits" *ngIf="bucket$ | async as bucket" [@listItemAnimation]="bucket.length"> <ng-container *ngIf="bucket.length > 0; else bucketEmptyMessage"> ... </ng-container> <ng-template #bucketEmptyMessage>...</ng-template> </div>
Ghi nhớ: Component Store giúp cô lập state, dễ test, nhẹ hơn store toàn cục.
🔗 Tổng kết chương
Kỹ thuật | Công cụ | Mục đích |
---|---|---|
Actions, Reducers | @ngrx/store | Cập nhật state |
Devtools | @ngrx/store-devtools | Debug actions và state |
Selectors | createFeatureSelector | Lấy dữ liệu từ state |
Effects | @ngrx/effects | Xử lý bất đồng bộ/API |
Component Store | @ngrx/component-store | Quản lý state cục bộ |
Quản lý state tốt là nền tảng để scale ứng dụng Angular một cách dễ dàng và có tổ chức.
Để lại một bình luận
Bạn phải đăng nhập để gửi bình luận.