NgRx Signal Store vs Classic NgRx ในปี 2026: ควรเลือกแบบไหน?
เปรียบเทียบเชิงลึกระหว่าง NgRx Signal Store และ Classic NgRx สำหรับการจัดการ state ใน Angular ปี 2026 เรียนรู้ข้อดี ข้อเสีย และเวลาที่ควรใช้แต่ละวิธี

NgRx Signal Store เป็นการเปลี่ยนแปลงพื้นฐานในการจัดการ state ของ Angular โดยแทนที่รูปแบบ actions-reducers-selectors แบบดั้งเดิมที่ได้รับแรงบันดาลใจจาก Redux ด้วยแนวทาง reactive ที่ใช้ signal บทความเปรียบเทียบนี้วิเคราะห์ทั้งสองโซลูชันเพื่อช่วยให้นักพัฒนา Angular ตัดสินใจเชิงสถาปัตยกรรมได้อย่างถูกต้องในปี 2026
ใช้ Signal Store สำหรับ state ระดับ feature และแอปพลิเคชันที่มีความซับซ้อนปานกลางเมื่อการลด boilerplate มีความสำคัญ ใช้ Classic NgRx สำหรับแอปพลิเคชัน enterprise ที่ต้องการ audit trail ที่เข้มงวด side effects ที่ซับซ้อน และการ debug ด้วย DevTools อย่างครอบคลุม
ทำความเข้าใจความแตกต่างเชิงสถาปัตยกรรม
Classic NgRx ปฏิบัติตามรูปแบบ Redux: actions อธิบาย events, reducers สร้าง state ใหม่ที่เป็น immutable และ selectors ดึงข้อมูลออกมา การเปลี่ยนแปลง state ทุกครั้งจะไหลผ่าน pipeline ที่คาดเดาได้และติดตามได้
Signal Store ใช้แนวทางที่แตกต่าง แทนที่จะ dispatch actions ผ่าน reducers state จะอยู่ใน reactive signals การอัปเดตเกิดขึ้นผ่าน methods ที่เรียก patchState() และค่าที่คำนวณได้มาจาก computed signals แทนที่จะเป็น selectors ที่ถูก memoize
| ด้าน | Classic NgRx | Signal Store | |------|--------------|---------------| | Mental Model | Redux/Flux | Reactive Services | | Boilerplate | สูง (actions, reducers, selectors, effects) | ต่ำ (state, methods, computed) | | Reactivity | RxJS Observables | Angular Signals | | DevTools | Redux DevTools เต็มรูปแบบ | Signal Store DevTools (ผ่าน plugin) | | ความยากในการเรียนรู้ | สูง | ปานกลาง | | ขนาด Bundle | ใหญ่กว่า | เล็กกว่า |
สำหรับนักพัฒนาที่คุ้นเคยกับ Angular Signals อยู่แล้ว Signal Store จะรู้สึกเหมือนส่วนขยายที่เป็นธรรมชาติของ reactive primitives ของ framework
Classic NgRx: รูปแบบ Redux ใน Angular
Classic NgRx จัดระเบียบการจัดการ state รอบๆ actions, reducers, selectors และ effects การแยกนี้บังคับให้เกิดความคาดเดาได้โดยแลกกับความยืดยาว
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 จัดกลุ่ม actions ที่เกี่ยวข้องภายใต้ source เดียว ปรับปรุงการอ่าน DevTools และลด boilerplate เมื่อเทียบกับการเรียก createAction แยกต่างหาก
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 ยังคงเป็น pure functions ที่ส่งคืน objects ของ state ใหม่ ฟังก์ชัน on() จัดการการจับคู่ action และการอัปเดต state อย่างกระชับ
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 มอบ memoization โดยอัตโนมัติ Selector selectPublishedBooks จะคำนวณใหม่เมื่อ array ของ books เปลี่ยนแปลงเท่านั้น ทำให้การอ่านมีประสิทธิภาพแม้ใน stores ที่อัปเดตบ่อย
NgRx Signal Store: การจัดการ State แบบ Signals-First
Signal Store รวม state, computed values และ methods เข้าไปใน injectable service เดียว เอกสารอย่างเป็นทางการของ NgRx อธิบายว่าเป็น "โซลูชันการจัดการ state ที่มีคุณสมบัติครบถ้วนพร้อมการสนับสนุน native สำหรับ 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),
}));
},
}))
);การจัดการ state ของ feature ทั้งหมดอยู่ในไฟล์เดียว คุณสมบัติ state กลายเป็น signals โดยอัตโนมัติ computed signals แทนที่ selectors และ methods แทนที่ dispatch actions
การจัดการ Entity ด้วย withEntities
ทั้งสองแนวทางสนับสนุน entity collections แต่คุณสมบัติ withEntities ของ Signal Store ลดโค้ดได้อย่างมาก
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));
},
}))
);คุณสมบัติ withEntities มอบ signals ids, entityMap และ entities โดยอัตโนมัติ Entity updaters เช่น addEntity, removeEntity และ updateEntity จัดการ mutations ของ collection
พร้อมที่จะพิชิตการสัมภาษณ์ Angular แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
Store Features แบบกำหนดเองสำหรับการนำกลับมาใช้ใหม่
ฟังก์ชัน signalStoreFeature ของ Signal Store ช่วยให้สามารถแยก cross-cutting concerns และนำกลับมาใช้ใหม่ข้าม stores ได้
import { computed } from '@angular/core';
import { signalStoreFeature, withComputed, withState } 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'),
}))
);
}
export function setPending(): { requestStatus: RequestStatus } {
return { requestStatus: 'pending' };
}
export function setFulfilled(): { requestStatus: RequestStatus } {
return { requestStatus: 'fulfilled' };
}
export function setError(): { requestStatus: RequestStatus } {
return { requestStatus: 'error' };
}คุณสมบัติแบบกำหนดเองนี้สามารถ compose เข้ากับ store ใดก็ได้ที่ต้องการติดตามสถานะการโหลด
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 ที่ถูก compose ตอนนี้จะ expose computed signals isPending(), isFulfilled() และ isError() โดยไม่ต้องทำซ้ำ logic
รูปแบบการรวม Component
Signal Store รวมเข้ากับ syntax ของ template Angular ได้อย่างเป็นธรรมชาติผ่านการอ่าน 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">กำลังโหลดหนังสือ...</div>
}
@if (store.isError()) {
<div class="error-message">ไม่สามารถโหลดหนังสือได้</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)">ลบ</button>
</div>
}
<p>ทั้งหมด: {{ store.totalBooks() }} เล่ม</p>
`,
})
export class BooksListComponent {
readonly store = inject(BooksStore);
constructor() {
this.store.loadBooks();
}
}ไม่ต้องจัดการ subscription ไม่ต้องใช้ async pipes Signals รวมเข้ากับ zoneless change detection ของ Angular เพื่อประสิทธิภาพที่ดีที่สุด
ข้อพิจารณาด้านประสิทธิภาพและขนาด Bundle
สถาปัตยกรรมที่เบากว่าของ Signal Store ส่งผลให้ bundles มีขนาดเล็กลง Package หลัก @ngrx/signals เพิ่มประมาณ 4KB gzipped เมื่อเทียบกับ 15-20KB สำหรับ stack classic NgRx เต็มรูปแบบ (@ngrx/store, @ngrx/effects, @ngrx/entity)
Signal Store ยังได้ประโยชน์จาก signal-based change detection ของ Angular Computed signals จะคำนวณใหม่เมื่อ dependencies เปลี่ยนแปลงเท่านั้น และ components จะ re-render เมื่อ signals ที่ใช้อยู่อัปเดตเท่านั้น Reactivity ที่ละเอียดนี้ขจัด cycles ของ change detection ที่ไม่จำเป็นซึ่งพบบ่อยใน patterns ที่ใช้ Observable
สำหรับแอปพลิเคชันที่ใช้ RxJS อย่างหนักอยู่แล้ว selectors ที่ใช้ Observable ของ classic NgRx จะรวมเข้ากับ pipelines ที่มีอยู่ได้อย่างราบรื่น
กลยุทธ์การย้ายจาก Classic ไปยัง Signal Store
การย้ายแอปพลิเคชัน classic NgRx ที่มีอยู่ไม่จำเป็นต้องเขียนใหม่ทั้งหมด ทั้งสองโซลูชันสามารถอยู่ร่วมกันได้ ช่วยให้สามารถนำไปใช้อย่างค่อยเป็นค่อยไป
- เริ่มต้นด้วย features ใหม่โดยใช้ Signal Store
- ย้าย feature modules ที่แยกออกมาทีละอัน
- เก็บ features ที่ซับซ้อนที่มี effects มากบน classic NgRx หากต้นทุนการย้ายเกินกว่าประโยชน์
- ใช้ NgRx Signals interop utilities เพื่อเชื่อมระหว่าง state ที่ใช้ Observable และที่ใช้ signal
Classic NgRx Store และ Signal Store สามารถทำงานคู่กันในแอปพลิเคชันเดียวกันได้ ทีม NgRx สนับสนุนทั้งสองแนวทางอย่างเป็นทางการและมี interop utilities สำหรับเชื่อมต่อระหว่างกัน
เมื่อไหร่ควรเลือกแต่ละแนวทาง
เลือก Signal Store เมื่อ:
- สร้างแอปพลิเคชัน Angular 20+ ใหม่
- จัดการ state ระดับ feature หรือ component
- ให้ความสำคัญกับประสบการณ์นักพัฒนาและการลด boilerplate
- ทีมมีประสบการณ์ NgRx จำกัด
- ขนาด bundle มีความสำคัญ
เลือก Classic NgRx เมื่อ:
- ดูแลรักษาแอปพลิเคชัน NgRx ที่มีอยู่
- ต้องการ action logging และ audit trails ที่เข้มงวด
- สร้าง workflows ที่ซับซ้อนที่มี side effects มากมาย
- ต้องการ Redux DevTools time-travel debugging เต็มรูปแบบ
- ทีมมีความเชี่ยวชาญ NgRx อย่างลึกซึ้ง
สำหรับพื้นฐานการจัดการ state การเข้าใจทั้งสองแนวทางเตรียมความพร้อมให้นักพัฒนาสำหรับ codebases Angular ที่หลากหลาย
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
บทสรุป
- Signal Store ลด boilerplate ในการจัดการ state 60-70% เมื่อเทียบกับ classic NgRx
- Classic NgRx ยังคงเป็นตัวเลือกที่ดีกว่าสำหรับแอปพลิเคชัน enterprise ที่ต้องการ audit trails ที่เข้มงวดและ side effects ที่ซับซ้อน
- ทั้งสองแนวทางสามารถอยู่ร่วมกันในแอปพลิเคชันเดียวกัน ช่วยให้ย้ายได้อย่างค่อยเป็นค่อยไป
- Computed signals ของ Signal Store มอบ memoization อัตโนมัติโดยไม่ต้องกำหนด selector อย่างชัดเจน
- Store features แบบกำหนดเอง (
signalStoreFeature) ช่วยให้ cross-cutting concerns สามารถนำกลับมาใช้ใหม่ได้ - สำหรับโปรเจกต์ Angular 20+ ใหม่ในปี 2026 Signal Store ควรเป็นตัวเลือกเริ่มต้นเว้นแต่ข้อกำหนดเฉพาะต้องการ classic NgRx
แชร์
บทความที่เกี่ยวข้อง

RxJS ใน Angular 2026: โอเปอเรเตอร์ Subject และ interop Signals
RxJS ใน Angular 2026: เชี่ยวชาญโอเปอเรเตอร์ Subject และรูปแบบ interop Signals ที่นักพัฒนา Angular ใช้จริงในโปรดักชัน พร้อมคำถามสัมภาษณ์ที่พบบ่อยที่สุด

Angular @defer ในปี 2026: Declarative Lazy Loading สำหรับแอปพลิเคชันที่เร็วขึ้น
เจาะลึก Angular @defer blocks สำหรับ declarative lazy loading พร้อม triggers ทุกประเภท prefetching, incremental hydration, พฤติกรรมบน SSR และรูปแบบการใช้งานจริงเพื่อเพิ่มประสิทธิภาพ

Angular 20 ในปี 2026: Resource API, httpResource และคำถามสัมภาษณ์
Angular 20 เปิดตัว httpResource และทำให้ Resource API เสถียรสำหรับการดึงข้อมูลแบบอิงกับ signal บทเรียนเชิงปฏิบัติครอบคลุม resource(), rxResource(), httpResource(), การตรวจสอบด้วย Zod และคำถามสัมภาษณ์ที่พบบ่อย