NgRx Signal Store vs Classic NgRx trong 2026: Nên Chọn Giải Pháp Nào?
So sánh toàn diện NgRx Signal Store và Classic NgRx cho quản lý state Angular. Tìm hiểu khi nào sử dụng mỗi cách tiếp cận với các ví dụ code thực tế.

NgRx Signal Store đại diện cho sự thay đổi căn bản trong quản lý state Angular, thay thế mô hình truyền thống actions-reducers-selectors theo Redux bằng cách tiếp cận reactive dựa trên signal. Với NgRx 22 (phát hành tháng 8/2026), Signal Store có thêm resource extensions, dynamic deep signals, và khả năng composability được cải thiện. Bài so sánh này phân tích cả hai giải pháp để giúp các nhà phát triển Angular đưa ra quyết định kiến trúc đúng đắn.
Sử dụng Signal Store cho state cấp feature, ứng dụng độ phức tạp trung bình, và tích hợp với Resource API native của Angular. Sử dụng Classic NgRx cho ứng dụng enterprise yêu cầu audit trail nghiêm ngặt, side effects phức tạp và debugging DevTools mở rộng.
Hiểu Rõ Sự Khác Biệt Kiến Trúc
Classic NgRx tuân theo mô hình Redux: actions mô tả events, reducers tạo ra state mới immutable, và selectors trích xuất dữ liệu. Mọi thay đổi state đều đi qua một pipeline có thể dự đoán và theo dõi được.
Signal Store có cách tiếp cận khác. Thay vì dispatch actions thông qua reducers, state tồn tại trong các signals reactive. Cập nhật xảy ra thông qua các methods gọi patchState(), và các giá trị dẫn xuất đến từ computed signals thay vì selectors được memoize.
| Khía Cạnh | Classic NgRx | Signal Store |
|---|---|---|
| Mô Hình Tư Duy | Redux/Flux | Reactive Services |
| Boilerplate | Cao (actions, reducers, selectors, effects) | Thấp (state, methods, computed) |
| Reactivity | RxJS Observables | Angular Signals |
| Resource API | Tích hợp thủ công | Native với @ngrx/signals/resource |
| DevTools | Redux DevTools Đầy Đủ | Signal Store DevTools (qua plugin) |
| Độ Khó Học | Dốc | Vừa Phải |
| Kích Thước Bundle | Lớn Hơn (~15-20KB gzipped) | Nhỏ Hơn (~4KB gzipped) |
Đối với các nhà phát triển đã quen thuộc với Angular Signals, Signal Store cảm giác như một phần mở rộng tự nhiên của các primitives reactive trong framework.
Classic NgRx: Mô Hình Redux trong Angular
Classic NgRx tổ chức quản lý state xung quanh actions, reducers, selectors và effects. Sự phân tách này đảm bảo tính dự đoán được với chi phí là độ dài dòng.
import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { Book } from './book.model';
export const BooksActions = createActionGroup({
source: 'Books',
events: {
'Load Books': emptyProps(),
'Load Books Success': props<{ books: Book[] }>(),
'Load Books Failure': props<{ error: string }>(),
'Add Book': props<{ book: Book }>(),
'Remove Book': props<{ id: string }>(),
},
});API createActionGroup nhóm các actions liên quan dưới một source duy nhất, cải thiện độ đọc của DevTools và giảm boilerplate so với các lời gọi createAction riêng lẻ.
import { createReducer, on } from '@ngrx/store';
import { BooksActions } from './books.actions';
import { Book } from './book.model';
export interface BooksState {
books: Book[];
loading: boolean;
error: string | null;
}
const initialState: BooksState = {
books: [],
loading: false,
error: null,
};
export const booksReducer = createReducer(
initialState,
on(BooksActions.loadBooks, (state) => ({
...state,
loading: true,
error: null,
})),
on(BooksActions.loadBooksSuccess, (state, { books }) => ({
...state,
books,
loading: false,
})),
on(BooksActions.loadBooksFailure, (state, { error }) => ({
...state,
loading: false,
error,
}))
);Reducers vẫn là các hàm thuần túy trả về đối tượng state mới. Hàm on() xử lý việc khớp action và cập nhật state một cách ngắn gọn.
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { BooksState } from './books.reducer';
export const selectBooksState = createFeatureSelector<BooksState>('books');
export const selectAllBooks = createSelector(
selectBooksState,
(state) => state.books
);
export const selectBooksLoading = createSelector(
selectBooksState,
(state) => state.loading
);
export const selectPublishedBooks = createSelector(
selectAllBooks,
(books) => books.filter((book) => book.published)
);Selectors cung cấp memoization tự động. Selector selectPublishedBooks chỉ tính toán lại khi mảng books thay đổi, làm cho việc đọc hiệu quả ngay cả trong các store được cập nhật thường xuyên.
NgRx Signal Store: Quản Lý State Signals-First
Signal Store hợp nhất state, computed values và methods vào một service injectable duy nhất. Tài liệu chính thức NgRx mô tả nó như "giải pháp quản lý state đầy đủ tính năng với hỗ trợ native cho Angular Signals."
import { computed, inject } from '@angular/core';
import { patchState, signalStore, withComputed, withMethods, withState } from '@ngrx/signals';
import { BooksService } from './books.service';
import { Book } from './book.model';
interface BooksState {
books: Book[];
loading: boolean;
error: string | null;
}
const initialState: BooksState = {
books: [],
loading: false,
error: null,
};
export const BooksStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withComputed(({ books }) => ({
publishedBooks: computed(() => books().filter((book) => book.published)),
totalBooks: computed(() => books().length),
})),
withMethods((store, booksService = inject(BooksService)) => ({
async loadBooks() {
patchState(store, { loading: true, error: null });
try {
const books = await booksService.getAll();
patchState(store, { books, loading: false });
} catch (error) {
patchState(store, { loading: false, error: 'Failed to load books' });
}
},
addBook(book: Book) {
patchState(store, ({ books }) => ({ books: [...books, book] }));
},
removeBook(id: string) {
patchState(store, ({ books }) => ({
books: books.filter((book) => book.id !== id),
}));
},
}))
);Toàn bộ quản lý state của feature nằm gọn trong một file. Các thuộc tính state tự động trở thành signals, computed signals thay thế selectors, và methods thay thế dispatch actions.
NgRx 22: Resource Extensions và withProps
NgRx 22 giới thiệu entry point @ngrx/signals/resource, cho phép tích hợp trực tiếp với Resource API của Angular. Tính năng withProps tập trung việc inject service, trong khi resource extensions xử lý các pattern async phổ biến.
import { computed, inject } from '@angular/core';
import { patchState, signalStore, withComputed, withMethods, withProps } from '@ngrx/signals';
import { extendResource, withPreviousValueOnLoading, withValueOnError } from '@ngrx/signals/resource';
import { httpResource } from '@angular/core';
import { BooksService } from './books.service';
import { Book } from './book.model';
export const BooksResourceStore = signalStore(
{ providedIn: 'root' },
// Inject services via withProps (private by convention with underscore)
withProps(() => ({
_booksService: inject(BooksService),
})),
// Set up resources with extensions
withProps((store) => ({
_booksResource: extendResource(
httpResource<Book[]>({
url: '/api/books',
defaultValue: [],
}),
withPreviousValueOnLoading(), // Keeps last value while reloading
withValueOnError([]) // Returns empty array on error
),
})),
// Expose read-only resource to consumers
withProps((store) => ({
booksResource: store._booksResource.asReadonly(),
})),
withComputed((store) => ({
books: computed(() => store.booksResource.value()),
isLoading: computed(() => store.booksResource.isLoading()),
publishedBooks: computed(() =>
store.booksResource.value().filter((book) => book.published)
),
}))
);Resource extensions giải quyết hai vấn đề phổ biến: flickering khi reload (được sửa bởi withPreviousValueOnLoading) và xử lý error state (được sửa bởi withValueOnError hoặc withPreviousValueOnError). Các extensions này có thể compose và có thể cung cấp globally qua provideResourceExtensions.
Quản Lý Entity với withEntities
Cả hai cách tiếp cận đều hỗ trợ collections entity, nhưng tính năng withEntities của Signal Store giảm đáng kể lượng code.
import { computed, inject } from '@angular/core';
import { patchState, signalStore, withComputed, withMethods } from '@ngrx/signals';
import {
addEntity,
removeEntity,
setAllEntities,
updateEntity,
withEntities,
} from '@ngrx/signals/entities';
import { BooksService } from './books.service';
import { Book } from './book.model';
export const BooksEntityStore = signalStore(
{ providedIn: 'root' },
withEntities<Book>(),
withComputed(({ entities }) => ({
publishedBooks: computed(() => entities().filter((book) => book.published)),
})),
withMethods((store, booksService = inject(BooksService)) => ({
async loadBooks() {
const books = await booksService.getAll();
patchState(store, setAllEntities(books));
},
addBook(book: Book) {
patchState(store, addEntity(book));
},
updateBook(id: string, changes: Partial<Book>) {
patchState(store, updateEntity({ id, changes }));
},
removeBook(id: string) {
patchState(store, removeEntity(id));
},
}))
);Tính năng withEntities tự động cung cấp các signals ids, entityMap và entities. Các entity updaters như addEntity, removeEntity và updateEntity xử lý các mutations của collection.
Sẵn sàng chinh phục phỏng vấn Angular?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Store Features Tùy Chỉnh Cho Tái Sử Dụng
Hàm signalStoreFeature của Signal Store cho phép trích xuất và tái sử dụng các cross-cutting concerns qua nhiều stores. NgRx 22 thêm SignalStoreFeatureType để đơn giản hóa việc trích xuất type.
import { computed } from '@angular/core';
import { signalStoreFeature, withComputed, withState, SignalStoreFeatureType } from '@ngrx/signals';
type RequestStatus = 'idle' | 'pending' | 'fulfilled' | 'error';
export function withRequestStatus() {
return signalStoreFeature(
withState<{ requestStatus: RequestStatus }>({ requestStatus: 'idle' }),
withComputed(({ requestStatus }) => ({
isPending: computed(() => requestStatus() === 'pending'),
isFulfilled: computed(() => requestStatus() === 'fulfilled'),
isError: computed(() => requestStatus() === 'error'),
}))
);
}
// NgRx 22: extract types without manual redeclaration
export type RequestStatusFeature = SignalStoreFeatureType<typeof withRequestStatus>;
export function setPending(): { requestStatus: RequestStatus } {
return { requestStatus: 'pending' };
}
export function setFulfilled(): { requestStatus: RequestStatus } {
return { requestStatus: 'fulfilled' };
}
export function setError(): { requestStatus: RequestStatus } {
return { requestStatus: 'error' };
}Feature tùy chỉnh này giờ có thể được compose vào bất kỳ store nào cần theo dõi trạng thái loading.
import { inject } from '@angular/core';
import { patchState, signalStore, withMethods } from '@ngrx/signals';
import { setAllEntities, withEntities } from '@ngrx/signals/entities';
import { withRequestStatus, setFulfilled, setPending, setError } from './with-request-status';
import { BooksService } from './books.service';
import { Book } from './book.model';
export const BooksStore = signalStore(
{ providedIn: 'root' },
withEntities<Book>(),
withRequestStatus(),
withMethods((store, booksService = inject(BooksService)) => ({
async loadBooks() {
patchState(store, setPending());
try {
const books = await booksService.getAll();
patchState(store, setAllEntities(books), setFulfilled());
} catch {
patchState(store, setError());
}
},
}))
);Store đã được compose giờ đây expose các computed signals isPending(), isFulfilled() và isError() mà không cần trùng lặp logic.
Theo Dõi State Đồng Bộ với watchState
NgRx 22 thêm hàm watchState cho việc theo dõi thay đổi state đồng bộ, hữu ích cho chức năng undo/redo hoặc lịch sử state.
import { inject, DestroyRef } from '@angular/core';
import { watchState } from '@ngrx/signals';
import { BooksStore } from './books.store';
export function setupStateHistory() {
const store = inject(BooksStore);
const destroyRef = inject(DestroyRef);
const history: unknown[] = [];
// Synchronous tracking: captures every state change, no coalescing
watchState(store, (state) => {
history.push(structuredClone(state));
});
return { history };
}Khác với việc theo dõi dựa trên effect gộp các updates lại, watchState chạy đồng bộ trên mỗi lần gọi patchState, ghi lại toàn bộ lịch sử state.
Mẫu Tích Hợp Component
Signal Store tích hợp tự nhiên với cú pháp template Angular thông qua việc đọc signal.
import { Component, inject } from '@angular/core';
import { BooksStore } from './books.store';
@Component({
selector: 'app-books-list',
standalone: true,
template: `
@if (store.isPending()) {
<div class="loading-spinner">Đang tải sách...</div>
}
@if (store.isError()) {
<div class="error-message">Không thể tải sách</div>
}
@for (book of store.entities(); track book.id) {
<div class="book-card">
<h3>{{ book.title }}</h3>
<p>{{ book.author }}</p>
<button (click)="store.removeBook(book.id)">Xóa</button>
</div>
}
<p>Tổng cộng: {{ store.totalBooks() }} sách</p>
`,
})
export class BooksListComponent {
readonly store = inject(BooksStore);
constructor() {
this.store.loadBooks();
}
}Không cần quản lý subscription. Không cần async pipes. Signals tích hợp với zoneless change detection của Angular để đạt hiệu suất tối ưu. Với OnPush là chiến lược change detection mặc định trong Angular 22, các component Signal Store hưởng lợi từ các cập nhật chính xác và chi tiết.
Cân Nhắc Về Hiệu Suất và Kích Thước Bundle
Kiến trúc nhẹ hơn của Signal Store dẫn đến bundles nhỏ hơn. Package core @ngrx/signals thêm khoảng 4KB gzipped, so với 15-20KB cho stack classic NgRx đầy đủ (@ngrx/store, @ngrx/effects, @ngrx/entity).
Signal Store cũng hưởng lợi từ signal-based change detection của Angular. Computed signals chỉ tính toán lại khi dependencies thay đổi, và components chỉ re-render khi signals được sử dụng cập nhật. Reactivity chi tiết này loại bỏ các chu kỳ change detection không cần thiết phổ biến trong các patterns dựa trên Observable.
Đối với các ứng dụng đã sử dụng RxJS nhiều, selectors dựa trên Observable của classic NgRx tích hợp mượt mà với các pipelines hiện có.
NgRx 22 yêu cầu Angular 22.x, TypeScript 6.0.x, và RxJS 6.5.x hoặc 7.5.x. Cập nhật với ng update @ngrx/signals@22 cho các dự án Signal Store.
Chiến Lược Di Chuyển Từ Classic Sang Signal Store
Di chuyển ứng dụng classic NgRx hiện có không yêu cầu viết lại toàn bộ. Cả hai giải pháp có thể tồn tại song song, cho phép áp dụng dần dần.
- Bắt đầu với các features mới sử dụng Signal Store
- Di chuyển các feature modules độc lập từng cái một
- Giữ các features phức tạp với nhiều effects trên classic NgRx nếu chi phí di chuyển vượt quá lợi ích
- Sử dụng các utilities interop NgRx Signals để kết nối giữa state dựa trên Observable và dựa trên signal
Classic NgRx Store và Signal Store có thể chạy song song trong cùng một ứng dụng. Đội ngũ NgRx chính thức hỗ trợ cả hai cách tiếp cận và cung cấp các utilities interop để kết nối chúng.
Khi Nào Chọn Mỗi Cách Tiếp Cận
Chọn Signal Store khi:
- Xây dựng ứng dụng Angular 22+ mới
- Quản lý state cấp feature hoặc component
- Tích hợp với Resource API và Signal Forms của Angular
- Ưu tiên trải nghiệm developer và giảm boilerplate
- Đội ngũ có kinh nghiệm NgRx hạn chế
- Kích thước bundle quan trọng
Chọn Classic NgRx khi:
- Bảo trì ứng dụng NgRx hiện có
- Yêu cầu action logging và audit trails nghiêm ngặt
- Xây dựng workflows phức tạp với side effects mở rộng
- Cần full Redux DevTools time-travel debugging
- Đội ngũ có chuyên môn NgRx sâu
Để hiểu các nguyên tắc cơ bản về quản lý state, việc hiểu cả hai cách tiếp cận chuẩn bị cho các nhà phát triển làm việc với đa dạng codebases Angular.
Sources
- NgRx v22 Release Announcement - Resource extensions, dynamic deep signals, SignalStoreFeatureType
- Angular 22 Feature Overview - Signal Forms stable, OnPush default, Resource API
- Using Angular's Resource API with NgRx Signal Store - withProps patterns, httpResource integration
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Signal Store vs Classic NgRx: Những Điểm Chính
- Signal Store giảm boilerplate quản lý state 60-70% so với classic NgRx
- NgRx 22 thêm resource extensions (
withPreviousValueOnLoading,withValueOnError) để xử lý state async mượt mà - Tính năng
withPropstập trung việc inject service, loại bỏ danh sách tham số lặp lại giữa các features watchStatecho phép theo dõi state đồng bộ cho undo/redo và lịch sử state- Classic NgRx vẫn là lựa chọn tốt hơn cho ứng dụng enterprise yêu cầu audit trails nghiêm ngặt và side effects phức tạp
- Cả hai cách tiếp cận có thể tồn tại song song trong cùng một ứng dụng, cho phép di chuyển dần dần
- Đối với các dự án Angular 22+ mới, Signal Store nên là lựa chọn mặc định trừ khi các yêu cầu cụ thể đòi hỏi classic NgRx
Bạn có tìm ra lỗi trong Angular không?
Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Viết bởi
Anthony Fillion-MailletNgười sáng lập SharpSkill
Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.
Cập nhật ngày 9 tháng 10, 2026
Thẻ
Chia sẻ
Bài viết liên quan

RxJS trong Angular 2026: Operator, Subject và Interop Signals
RxJS trong Angular 2026: nắm vững các operator, Subject và mẫu interop Signals mà lập trình viên Angular dùng trong production, cùng những câu hỏi phỏng vấn hay gặp nhất.

Angular Resource API và httpResource: Hướng dẫn đầy đủ (Ổn định trong Angular 22)
Resource API và httpResource của Angular hiện đã ổn định kể từ Angular 22. Hướng dẫn này bao gồm resource(), rxResource(), httpResource(), helper chain() cho resource phụ thuộc, validation Zod, và các câu hỏi phỏng vấn.

Dependency Injection Nang Cao trong Angular 2026: Providers, Tokens va Cau Hoi Phong Van
Tim hieu sau ve he thong dependency injection cua Angular bao gom chien luoc provider, InjectionToken, injector phan cap va cac cau hoi phong van thuong gap cho developer Angular co kinh nghiem.