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

NgRx Signal Store เป็นการเปลี่ยนแปลงพื้นฐานในการจัดการ state ของ Angular โดยแทนที่รูปแบบ actions-reducers-selectors แบบดั้งเดิมที่ได้รับแรงบันดาลใจจาก Redux ด้วยแนวทาง reactive ที่ใช้ signal ด้วย NgRx 22 (เปิดตัวเดือนสิงหาคม 2026) Signal Store ได้รับ resource extensions, dynamic deep signals และ composability ที่ดีขึ้น บทความเปรียบเทียบนี้วิเคราะห์ทั้งสองโซลูชันเพื่อช่วยให้นักพัฒนา Angular ตัดสินใจเชิงสถาปัตยกรรมได้อย่างถูกต้อง
ใช้ Signal Store สำหรับ state ระดับ feature แอปพลิเคชันที่มีความซับซ้อนปานกลาง และการผสานรวมกับ Resource API native ของ Angular ใช้ 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 |
| Resource API | ผสานรวมแบบ manual | Native กับ @ngrx/signals/resource |
| DevTools | Redux DevTools เต็มรูปแบบ | Signal Store DevTools (ผ่าน plugin) |
| ความยากในการเรียนรู้ | สูง | ปานกลาง |
| ขนาด Bundle | ใหญ่กว่า (~15-20KB gzipped) | เล็กกว่า (~4KB gzipped) |
สำหรับนักพัฒนาที่คุ้นเคยกับ 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
NgRx 22: Resource Extensions และ withProps
NgRx 22 แนะนำ entry point @ngrx/signals/resource ซึ่งเปิดให้ผสานรวมโดยตรงกับ Resource API ของ Angular คุณสมบัติ withProps รวมศูนย์การ inject service ในขณะที่ resource extensions จัดการ patterns async ที่พบบ่อย
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 แก้ปัญหาที่พบบ่อยสองประการ: flickering ระหว่าง reload (แก้ไขโดย withPreviousValueOnLoading) และการจัดการ error state (แก้ไขโดย withValueOnError หรือ withPreviousValueOnError) extensions เหล่านี้สามารถ compose และสามารถให้แบบ global ผ่าน provideResourceExtensions
การจัดการ 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 ได้ NgRx 22 เพิ่ม SignalStoreFeatureType เพื่อทำให้การ extract 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' };
}คุณสมบัติแบบกำหนดเองนี้สามารถ 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
การติดตาม State แบบ Synchronous ด้วย watchState
NgRx 22 เพิ่มฟังก์ชัน watchState สำหรับการติดตามการเปลี่ยนแปลง state แบบ synchronous ซึ่งมีประโยชน์สำหรับฟังก์ชัน undo/redo หรือประวัติ 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 };
}ต่างจากการติดตามแบบ effect ที่รวม updates เข้าด้วยกัน watchState ทำงานแบบ synchronous ในทุกการเรียก patchState โดยจับทุกประวัติ state
รูปแบบการรวม 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 เพื่อประสิทธิภาพที่ดีที่สุด ด้วย OnPush เป็นกลยุทธ์ change detection เริ่มต้นใน Angular 22 components ของ Signal Store ได้รับประโยชน์จากการอัปเดตที่แม่นยำและละเอียด
ข้อพิจารณาด้านประสิทธิภาพและขนาด 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 ที่มีอยู่ได้อย่างราบรื่น
NgRx 22 ต้องการ Angular 22.x, TypeScript 6.0.x และ RxJS 6.5.x หรือ 7.5.x อัปเดตด้วย ng update @ngrx/signals@22 สำหรับโปรเจกต์ Signal Store
กลยุทธ์การย้ายจาก 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 22+ ใหม่
- จัดการ state ระดับ feature หรือ component
- ผสานรวมกับ Resource API และ Signal Forms ของ Angular
- ให้ความสำคัญกับประสบการณ์นักพัฒนาและการลด boilerplate
- ทีมมีประสบการณ์ NgRx จำกัด
- ขนาด bundle มีความสำคัญ
เลือก Classic NgRx เมื่อ:
- ดูแลรักษาแอปพลิเคชัน NgRx ที่มีอยู่
- ต้องการ action logging และ audit trails ที่เข้มงวด
- สร้าง workflows ที่ซับซ้อนที่มี side effects มากมาย
- ต้องการ Redux DevTools time-travel debugging เต็มรูปแบบ
- ทีมมีความเชี่ยวชาญ NgRx อย่างลึกซึ้ง
สำหรับพื้นฐานการจัดการ state การเข้าใจทั้งสองแนวทางเตรียมความพร้อมให้นักพัฒนาสำหรับ 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
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
Signal Store vs Classic NgRx: ประเด็นสำคัญ
- Signal Store ลด boilerplate ในการจัดการ state 60-70% เมื่อเทียบกับ classic NgRx
- NgRx 22 เพิ่ม resource extensions (
withPreviousValueOnLoading,withValueOnError) สำหรับการจัดการ state async อย่างราบรื่น - คุณสมบัติ
withPropsรวมศูนย์การ inject service ขจัดรายการ parameters ที่ซ้ำกันข้าม features watchStateเปิดให้ติดตาม state แบบ synchronous สำหรับ undo/redo และประวัติ state- Classic NgRx ยังคงเป็นตัวเลือกที่ดีกว่าสำหรับแอปพลิเคชัน enterprise ที่ต้องการ audit trails ที่เข้มงวดและ side effects ที่ซับซ้อน
- ทั้งสองแนวทางสามารถอยู่ร่วมกันในแอปพลิเคชันเดียวกัน ช่วยให้ย้ายได้อย่างค่อยเป็นค่อยไป
- สำหรับโปรเจกต์ Angular 22+ ใหม่ Signal Store ควรเป็นตัวเลือกเริ่มต้นเว้นแต่ข้อกำหนดเฉพาะต้องการ classic NgRx
คุณหาบั๊กใน Angular เจอไหม
โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

เขียนโดย
Anthony Fillion-Mailletผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 9 ตุลาคม 2569
แท็ก
แชร์
บทความที่เกี่ยวข้อง

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

Angular Resource API และ httpResource: คู่มือครบถ้วน (เสถียรใน Angular 22)
Resource API และ httpResource ของ Angular เสถียรแล้วตั้งแต่ Angular 22 คู่มือนี้ครอบคลุม resource(), rxResource(), httpResource(), helper chain() สำหรับ resource ที่ขึ้นต่อกัน, การตรวจสอบ Zod และคำถามสัมภาษณ์

Dependency Injection ขั้นสูงใน Angular 2026: Providers, Tokens และคำถามสัมภาษณ์
เรียนรู้ระบบ dependency injection ของ Angular อย่างลึกซึ้ง รวมถึงกลยุทธ์ provider, InjectionToken, injector แบบลำดับชั้น และคำถามสัมภาษณ์ที่พบบ่อยสำหรับนักพัฒนา Angular ที่มีประสบการณ์