Table Of Content
- Giới thiệu
- Tại sao cần NgRx?
- Kiến trúc NgRx bao gồm
- Quy trình hoạt động của NgRx
- Cài đặt NgRx
- Ví dụ thực tế: Quản lý danh sách sách
- a. Khởi tạo State, Action và Reducer
- models/book.model.ts
- store/actions/book.actions.ts
- store/reducers/book.reducer.ts
- b. Khai báo Store trong AppModule
- c. Tạo Effects để gọi API
- services/book.service.ts
- store/effects/book.effects.ts
- d. Sử dụng Store trong Component
- book-list.component.ts
- e. Hiển thị trong HTML
- book-list.component.html
- Kết luận
Giới thiệu
Khi phát triển các ứng dụng Angular quy mô lớn, việc quản lý state (trạng thái dữ liệu) trở nên khó khăn và phức tạp. Các thao tác chia sẻ dữ liệu giữa nhiều component, tính bất đồng bộ và khả năng tracking thay đổi state yêu cầu một giải pháp có tổ chức.
NgRx (Đọc là “Ng-R-X”) là một thư viện quản lý state theo mô hình Redux, kết hợp với Reactive Programming từ RxJS. NgRx cung cấp một kiến trúc rõ ràng, đơn hướng, dễ kiểm soát và mở rộng. Trong bài viết này, chúng ta sẽ đi sâu vào cách sử dụng NgRx để quản lý state trong Angular thông qua ví dụ thực tế và phân tích chi tiết từng phần.
Tại sao cần NgRx?
- Quản lý state trung tâm: Dữ liệu được quản lý tại một store duy nhất, dễ dàng theo dõi và kiểm soát.
- Tính predictability: Do state chỉ thay đổi qua actions + reducers, state trở nên dễ dự đoán hơn.
- Debug + Time Travel: Tích hợp Redux DevTools cho phép xem lại quá trình thay đổi state một cách trực quan.
- Unidirectional data flow: Dữ liệu luôn di chuyển từ Actions → Reducers → Store → View, giúp ứng dụng dễ hiểu hơn và giảm lỗi do luồng dữ liệu phức tạp.
- Dễ dàng kiểm thử (unit test): Mỗi phần (actions, reducers, effects) đều có thể viết test riêng biệt.
Kiến trúc NgRx bao gồm:
- Actions: Mô tả hành động có thể xảy ra trong ứng dụng (ví dụ: Load Data, Add Item, Delete Item…)
- Reducers: Nhận state hiện tại và một action, trả về state mới tương ứng.
- Store: Nơi lưu trữ state toàn cục của ứng dụng, giống như một single source of truth.
- Selectors: Các hàm lấy dữ liệu cần thiết từ state, có thể tái sử dụng và kết hợp.
- Effects: Xử lý các side effects như gọi API, tương tác với router, hoặc các logic bất đồng bộ.
Quy trình hoạt động của NgRx
Khi một người dùng tương tác với ứng dụng, ví dụ như nhấn nút để tải dữ liệu hoặc thêm một mục mới, quy trình hoạt động của NgRx diễn ra theo các bước sau:
- Component dispatch Action: Component gọi
store.dispatch(action)
để gửi một hành động (action), ví dụ:loadBooks()
. - Effect lắng nghe Action (nếu có side effect): Nếu có side effect cần xử lý như gọi API, thì effect sẽ lắng nghe action thông qua
ofType()
và thực hiện logic cần thiết. Sau đó, nó có thể dispatch action khác nhưloadBooksSuccess()
hoặcloadBooksFailure()
. - Reducer cập nhật State: Khi một action được gửi đến store, reducer sẽ xử lý action này và trả về state mới. Reducer là pure function, không chứa side effect.
- Store cập nhật dữ liệu: Store nhận state mới từ reducer và cập nhật lại store nội bộ.
- Component và Selectors nhận dữ liệu mới: Các component đã subscribe vào store (thông qua
store.select(...)
) sẽ nhận được state mới và tự động cập nhật UI. Các selectors có thể được dùng để trích xuất dữ liệu cụ thể từ state.
Quy trình này giúp đảm bảo luồng dữ liệu luôn đơn hướng và rõ ràng, dễ kiểm soát và dễ debug.
Cài đặt NgRx
ng add @ngrx/store
ng add @ngrx/effects
ng add @ngrx/store-devtools
Ví dụ thực tế: Quản lý danh sách sách
a. Khởi tạo State, Action và Reducer
models/book.model.ts
export interface Book {
id: number;
title: string;
author: string;
publishedDate?: string;
}
store/actions/book.actions.ts
import { createAction, props } from '@ngrx/store';
import { Book } from '../../models/book.model';
export const loadBooks = createAction('[Book List] Load Books');
export const loadBooksSuccess = createAction('[Book List] Load Books Success', props<{ books: Book[] }>());
export const addBook = createAction('[Book List] Add Book', props<{ book: Book }>());
export const deleteBook = createAction('[Book List] Delete Book', props<{ id: number }>());
store/reducers/book.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { Book } from '../../models/book.model';
import * as BookActions from '../actions/book.actions';
export const initialState: Book[] = [];
export const bookReducer = createReducer(
initialState,
on(BookActions.loadBooksSuccess, (state, { books }) => [...books]),
on(BookActions.addBook, (state, { book }) => [...state, book]),
on(BookActions.deleteBook, (state, { id }) => state.filter(book => book.id !== id))
);
b. Khai báo Store trong AppModule
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { BookEffects } from './store/effects/book.effects';
import { bookReducer } from './store/reducers/book.reducer';
@NgModule({
imports: [
StoreModule.forRoot({ books: bookReducer }),
EffectsModule.forRoot([BookEffects]),
StoreDevtoolsModule.instrument({ maxAge: 25 }),
]
})
export class AppModule {}
c. Tạo Effects để gọi API
services/book.service.ts
@Injectable({ providedIn: 'root' })
export class BookService {
constructor(private http: HttpClient) {}
getAll(): Observable<Book[]> {
return this.http.get<Book[]>('/api/books');
}
}
store/effects/book.effects.ts
@Injectable()
export class BookEffects {
constructor(private actions$: Actions, private bookService: BookService) {}
loadBooks$ = createEffect(() =>
this.actions$.pipe(
ofType(loadBooks),
mergeMap(() =>
this.bookService.getAll().pipe(
map(books => loadBooksSuccess({ books })),
catchError(() => of({ type: '[Book List] Load Books Failure' }))
)
)
)
);
}
d. Sử dụng Store trong Component
book-list.component.ts
@Component({ selector: 'app-book-list', templateUrl: './book-list.component.html' })
export class BookListComponent implements OnInit {
books$ = this.store.select(state => state.books);
constructor(private store: Store<{ books: Book[] }>) {}
ngOnInit(): void {
this.store.dispatch(loadBooks());
}
addBook() {
const newBook: Book = {
id: Date.now(),
title: 'Domain-Driven Design',
author: 'Eric Evans',
publishedDate: '2003-08-30'
};
this.store.dispatch(addBook({ book: newBook }));
}
deleteBook(bookId: number) {
this.store.dispatch(deleteBook({ id: bookId }));
}
}
e. Hiển thị trong HTML
book-list.component.html
<div *ngFor="let book of books$ | async">
<strong>{{ book.title }}</strong> - {{ book.author }} ({{ book.publishedDate }})
<button (click)="deleteBook(book.id)">Xoá</button>
</div>
<button (click)="addBook()">Thêm sách</button>
Kết luận
NgRx là công cụ mạnh mẽ và đáng tin cậy trong Angular để quản lý state một cách có hệ thống, dự đoán và dễ duy trì. Khi ứng dụng mở rộng, NgRx giúp kiểm soát sự phức tạp bằng cách tách biệt rõ ràng từng chức năng. Tuy nhiên, do tính chất boilerplate, hãy cân nhắc kỹ trước khi sử dụng để tránh over-engineering ở những ứng dụng nhỏ hoặc ít thay đổi state.
Việc hiểu và áp dụng đúng cách sẽ giúp bạn tối ưu hóa hiệu suất, cải thiện khả năng kiểm thử và tăng khả năng bảo trì của ứng dụng Angular dài hạn.
Trong các dự án thực tế, NgRx tỏa sáng nhất khi bạn cần xử lý dữ liệu bất đồng bộ phức tạp, ví dụ như gọi nhiều API song song, điều hướng sau khi lưu dữ liệu, đồng bộ trạng thái giữa các tab, v.v… Với công cụ như Store Devtools và khả năng chia nhỏ các phần trong hệ thống (actions, reducers, selectors, effects), bạn có thể dễ dàng mở rộng hoặc tái cấu trúc ứng dụng một cách linh hoạt và có kiểm soát.
No Comment! Be the first one.