NgRx Signal Store vs Classic NgRx ในปี 2026: ควรเลือกแบบไหน?

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

เปรียบเทียบการจัดการ state NgRx Signal Store vs Classic NgRx 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 NgRxSignal Store
Mental ModelRedux/FluxReactive Services
Boilerplateสูง (actions, reducers, selectors, effects)ต่ำ (state, methods, computed)
ReactivityRxJS ObservablesAngular Signals
Resource APIผสานรวมแบบ manualNative กับ @ngrx/signals/resource
DevToolsRedux 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 การแยกนี้บังคับให้เกิดความคาดเดาได้โดยแลกกับความยืดยาว

books.actions.tstypescript
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 แยกต่างหาก

books.reducer.tstypescript
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 อย่างกระชับ

books.selectors.tstypescript
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"

books.store.tstypescript
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 ที่พบบ่อย

books-resource.store.tstypescript
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 ลดโค้ดได้อย่างมาก

books-entity.store.tstypescript
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 ง่ายขึ้น

with-request-status.tstypescript
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 ใดก็ได้ที่ต้องการติดตามสถานะการโหลด

books-with-status.store.tstypescript
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

state-history.tstypescript
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

books-list.component.tstypescript
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

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

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

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

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 9 ตุลาคม 2569

แท็ก

#angular
#ngrx
#state-management
#signals
#typescript

แชร์

บทความที่เกี่ยวข้อง

RxJS ใน Angular 2026 โอเปอเรเตอร์ Subject และ interop Signals

RxJS ใน Angular 2026: โอเปอเรเตอร์ Subject และ interop Signals

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

บทเรียน Angular 20 Resource API และ httpResource สำหรับการดึงข้อมูลเชิงปฏิกิริยาด้วย signal

Angular Resource API และ httpResource: คู่มือครบถ้วน (เสถียรใน Angular 22)

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

Angular Dependency Injection Advanced Providers Tokens

Dependency Injection ขั้นสูงใน Angular 2026: Providers, Tokens และคำถามสัมภาษณ์

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