# NgRx 狀態管理基礎

在 Angular 專案中，隨著應用程式複雜度增加，狀態管理變得越來越重要。NgRx 提供了一套完整的狀態管理解決方案，基於 Redux 模式設計，特別適合中大型專案。這篇筆記整理了 NgRx 的核心概念和使用方式。

### 核心概念

NgRx 建立在幾個關鍵概念上，組成一個完整的單向資料流：

#### Store

Store 是整個應用程式的中央狀態容器，就像是一個保險箱，存放應用程式中所有的狀態資料。

```typescript
// 注入 Store
private readonly cartStore: Store<{ cart: CartState }> = inject(Store);
```

Store 是一個泛型類別，泛型參數定義了它管理的狀態結構。在上面的例子中，它管理一個包含 `cart` 屬性的狀態，該屬性的類型是 `CartState`。

#### State

State 就是應用程式的資料結構，通常用介面定義。

```typescript
// 定義狀態介面
export interface CartState {
  items: CartItem[];
  totalAmount: number;
  discount: number;
  isCheckoutEnabled: boolean;
}

// 定義初始狀態
export const initialState: CartState = {
  items: [],
  totalAmount: 0,
  discount: 0,
  isCheckoutEnabled: false
};
```

#### Actions

Actions 是狀態變更的指令，描述「發生了什麼事」。它們是普通的 JavaScript 物件，包含一個 type 屬性和可選的 payload。

```typescript
// 定義動作類型
export const addItem = createAction(
  '[Cart] Add Item',
  props<{ item: CartItem }>()
);

export const removeItem = createAction(
  '[Cart] Remove Item',
  props<{ productId: string }>()
);

export const clearCart = createAction('[Cart] Clear Cart');

// 使用動作
this.cartStore.dispatch(CartActions.addItem({ item: newItem }));
this.cartStore.dispatch(CartActions.removeItem({ productId: 'product-123' }));
```

#### Reducers

Reducers 是純函數，負責根據 Actions 計算新的狀態。它們接收當前狀態和一個動作，然後返回新的狀態。

```typescript
// 定義 reducer
export const cartReducer = createReducer(
  initialState,

  // 加入商品
  on(CartActions.addItem, (state, { item }) => {
    // 檢查是否已有相同商品
    const existingItemIndex = state.items.findIndex(i => i.productId === item.productId);
    
    let newItems: CartItem[];
    if (existingItemIndex >= 0) {
      // 若已有相同商品，更新數量
      newItems = [...state.items];
      newItems[existingItemIndex] = {
        ...newItems[existingItemIndex],
        quantity: newItems[existingItemIndex].quantity + item.quantity
      };
    } else {
      // 若沒有相同商品，直接加入
      newItems = [...state.items, item];
    }
    
    // 計算新總金額
    const newTotalAmount = calculateTotalAmount(newItems, state.discount);
    
    return {
      ...state,
      items: newItems,
      totalAmount: newTotalAmount,
      isCheckoutEnabled: newItems.length > 0
    };
  }),

  // 移除商品
  on(CartActions.removeItem, (state, { productId }) => {
    const newItems = state.items.filter(item => item.productId !== productId);
    const newTotalAmount = calculateTotalAmount(newItems, state.discount);
    
    return {
      ...state,
      items: newItems,
      totalAmount: newTotalAmount,
      isCheckoutEnabled: newItems.length > 0
    };
  }),

  // 清空購物車
  on(CartActions.clearCart, () => ({
    ...initialState
  }))
);

// 計算總金額的輔助函數
function calculateTotalAmount(items: CartItem[], discount: number): number {
  const subtotal = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
  return subtotal * (1 - discount/100);
}
```

#### Selectors

Selectors 是純函數，用於從 Store 中提取特定的狀態片段。它們可以被記憶化，提高性能。

```typescript
// 特徵選擇器 - 找到整個「購物車狀態」區塊
export const selectCartState = createFeatureSelector<CartState>('cart');

// 取得購物車項目
export const selectCartItems = createSelector(
  selectCartState,
  (state: CartState) => state.items
);

// 取得購物車總金額
export const selectTotalAmount = createSelector(
  selectCartState,
  (state: CartState) => state.totalAmount
);

// 取得購物車項目數量
export const selectItemCount = createSelector(
  selectCartItems,
  (items: CartItem[]) => items.reduce((count, item) => count + item.quantity, 0)
);

// 取得結帳按鈕啟用狀態
export const selectCheckoutEnabled = createSelector(
  selectCartState,
  (state: CartState) => state.isCheckoutEnabled
);
```

### 完整資料流程

NgRx 的資料流向是單向的，形成一個清晰的循環：

1.  **元件發出動作**：使用 `dispatch` 方法發送動作

    ```typescript
    addToCart(product: Product, quantity: number = 1) {
      const cartItem: CartItem = {
        productId: product.id,
        name: product.name,
        price: product.price,
        quantity: quantity,
        imageUrl: product.imageUrl
      };
      this.cartStore.dispatch(CartActions.addItem({ item: cartItem }));
    }
    ```
2.  **Reducer 處理動作**：根據動作類型更新狀態

    ```typescript
    on(CartActions.addItem, (state, { item }) => {
      // 計算新狀態...
      return {
        ...state,
        items: newItems,
        totalAmount: newTotalAmount
      };
    })
    ```
3.  **選擇器提取狀態**：元件使用選擇器訂閱狀態變化

    ```typescript
    // 使用 select 方法和選擇器獲取狀態
    this.cartStore.select(selectCartItems).subscribe(items => {
      this.cartItems = items;
      this.updateCartView();
    });

    // 或使用 Angular Signals (Angular 16+)
    cartItems = toSignal(this.cartStore.select(selectCartItems));
    totalAmount = toSignal(this.cartStore.select(selectTotalAmount));
    itemCount = toSignal(this.cartStore.select(selectItemCount));
    ```
4. **元件響應變化**：當狀態變化時，所有訂閱的元件自動更新

### 實際使用範例

#### 購物車功能實作

以下是實作購物車功能的範例，展示了如何使用 NgRx 管理購物車狀態：

```typescript
@Component({
  selector: 'app-product-detail',
  template: `
    <div class="product-card">
      <img [src]="product.imageUrl" [alt]="product.name">
      <h2>{{ product.name }}</h2>
      <p class="price">NT$ {{ product.price | number:'1.0-0' }}</p>
      <p class="description">{{ product.description }}</p>
      
      <div class="quantity-selector">
        <button (click)="decreaseQuantity()">-</button>
        <span>{{ quantity }}</span>
        <button (click)="increaseQuantity()">+</button>
      </div>
      
      <button 
        class="add-to-cart-btn" 
        [disabled]="product.stock <= 0"
        (click)="addToCart()">
        加入購物車
      </button>
      
      <div *ngIf="(itemCount() > 0)" class="cart-summary">
        購物車: {{ itemCount() }} 件商品，總計: NT$ {{ totalAmount() | number:'1.0-0' }}
        <button (click)="goToCheckout()" [disabled]="!checkoutEnabled()">結帳</button>
      </div>
    </div>
  `
})
export class ProductDetailComponent {
  @Input() product!: Product;
  quantity = 1;
  
  // 使用 Signals 取得購物車狀態
  itemCount = toSignal(this.cartStore.select(selectItemCount));
  totalAmount = toSignal(this.cartStore.select(selectTotalAmount));
  checkoutEnabled = toSignal(this.cartStore.select(selectCheckoutEnabled));
  
  constructor(
    private cartStore: Store<{ cart: CartState }>,
    private router: Router,
    private notificationService: NotificationService
  ) {}
  
  decreaseQuantity(): void {
    if (this.quantity > 1) {
      this.quantity--;
    }
  }
  
  increaseQuantity(): void {
    if (this.quantity < this.product.stock) {
      this.quantity++;
    }
  }
  
  addToCart(): void {
    // 確認商品庫存足夠
    if (this.product.stock < this.quantity) {
      this.notificationService.showError('庫存不足');
      return;
    }
    
    const cartItem: CartItem = {
      productId: this.product.id,
      name: this.product.name,
      price: this.product.price,
      quantity: this.quantity,
      imageUrl: this.product.imageUrl
    };
    
    // 發送加入購物車動作
    this.cartStore.dispatch(CartActions.addItem({ item: cartItem }));
    this.notificationService.showSuccess('已加入購物車');
    
    // 重設數量
    this.quantity = 1;
  }
  
  goToCheckout(): void {
    this.router.navigate(['/checkout']);
  }
}quantity = 1;
  }
  
  goToCheckout(): void {
    this.router.navigate(['/checkout']);
  }
}
```

這個購物車功能範例完整展示了 NgRx 的使用流程：

1. 定義購物車狀態（CartState）包含商品清單、總金額等資訊
2. 創建動作（addItem, removeItem, clearCart）用於描述操作
3. 實作 Reducer 定義如何根據各種動作更新狀態
4. 使用選擇器提取所需的狀態片段（商品數量、總金額等）
5. 元件透過 dispatch 發送動作，並使用選擇器訂閱狀態變化

與傳統的服務+RxJS 實作相比，NgRx 方案提供了更清晰的結構和單向資料流程，讓狀態變更更可預測，也更容易追蹤和除錯。

### NgRx 常用 API 參考

這個區塊整理了 NgRx 中最常用的 API，方便快速查閱和使用。

#### 初始化與設定

```typescript
// 在 app.module.ts 或 app.config.ts 中設定 NgRx Store
import { StoreModule } from '@ngrx/store';

// Angular 16+ 使用 provideStore
providers: [
  provideStore({ cart: cartReducer, user: userReducer }),
  provideState({ name: 'products', reducer: productsReducer }),
]

// 舊版 Angular 使用 StoreModule
imports: [
  StoreModule.forRoot({ cart: cartReducer, user: userReducer }),
  StoreModule.forFeature('products', productsReducer),
]
```

#### 定義 Action

```typescript
// 基本 Action（無參數）
export const loadProducts = createAction('[Product] Load Products');

// 帶參數的 Action
export const loadProductsSuccess = createAction(
  '[Product] Load Products Success',
  props<{ products: Product[] }>()
);

// 帶錯誤的 Action
export const loadProductsFailure = createAction(
  '[Product] Load Products Failure',
  props<{ error: any }>()
);
```

#### 定義 Reducer

```typescript
// 使用 createReducer
export const productsReducer = createReducer(
  initialState,
  
  // 處理單一 Action
  on(ProductActions.loadProducts, state => ({
    ...state,
    loading: true
  })),
  
  // 處理帶參數的 Action
  on(ProductActions.loadProductsSuccess, (state, { products }) => ({
    ...state,
    products,
    loading: false,
    error: null
  })),
  
  // 處理錯誤 Action
  on(ProductActions.loadProductsFailure, (state, { error }) => ({
    ...state,
    loading: false,
    error
  }))
);

// 轉換為相容舊版 NgRx 的形式
export function reducer(state: ProductsState | undefined, action: Action) {
  return productsReducer(state, action);
}
```

#### 定義 Selector

```typescript
// 基本特徵選擇器
export const selectProductState = createFeatureSelector<ProductsState>('products');

// 簡單選擇器
export const selectAllProducts = createSelector(
  selectProductState,
  state => state.products
);

// 帶參數的選擇器
export const selectProductById = (productId: string) => createSelector(
  selectAllProducts,
  products => products.find(product => product.id === productId)
);

// 組合多個選擇器
export const selectProductsWithCategories = createSelector(
  selectAllProducts,
  selectAllCategories,
  (products, categories) => {
    return products.map(product => ({
      ...product,
      categoryName: categories.find(c => c.id === product.categoryId)?.name || 'Unknown'
    }));
  }
);

// 使用 select 方法獲取狀態（Observable 形式）
this.products$ = this.store.select(selectAllProducts);

// 使用 toSignal 轉換為 Signal (Angular 16+)
products = toSignal(this.store.select(selectAllProducts), { initialValue: [] });
isLoading = toSignal(this.store.select(selectProductsLoading), { initialValue: false });
```

#### 發送 Action

```typescript
// 不帶參數的 Action
this.store.dispatch(ProductActions.loadProducts());

// 帶參數的 Action
this.store.dispatch(ProductActions.addProduct({ product: newProduct }));

// 串連多個 Action
addProductAndNavigate(product: Product) {
  this.store.dispatch(ProductActions.addProduct({ product }));
  this.store.dispatch(RouterActions.go({ path: ['/products'] }));
}
```

#### 使用 Effects（副作用）

```typescript
// 安裝 Effects 模組
providers: [
  provideEffects(ProductEffects)
]

// 定義 Effect
@Injectable()
export class ProductEffects {
  constructor(
    private actions$: Actions,
    private productService: ProductService,
    private store: Store
  ) {}
  
  // 載入產品 Effect
  loadProducts$ = createEffect(() => this.actions$.pipe(
    ofType(ProductActions.loadProducts),
    switchMap(() => this.productService.getProducts().pipe(
      map(products => ProductActions.loadProductsSuccess({ products })),
      catchError(error => of(ProductActions.loadProductsFailure({ error })))
    ))
  ));
  
  // 不分發 Action 的 Effect
  logActions$ = createEffect(() => this.actions$.pipe(
    tap(action => console.log('Action: ', action)),
  ), { dispatch: false });
}
```

#### 使用 Router Store

```typescript
// 設定 Router Store
providers: [
  provideRouterStore()
]

// 使用 Router Selectors
selectCurrentRoute = this.store.select(selectRouteParams);
productId = toSignal(this.store.select(selectRouteParam('id')));

// 導航
this.store.dispatch(routerNavigatedAction({
  payload: {
    routerState: { url: '/products/1' },
    event: {}
  }
}));
```

#### 使用 Entity Adapter

```typescript
// 創建 Entity Adapter
export const productsAdapter = createEntityAdapter<Product>({
  selectId: product => product.id,
  sortComparer: (a, b) => a.name.localeCompare(b.name)
});

// 初始狀態
export const initialState: ProductsState = productsAdapter.getInitialState({
  loading: false,
  error: null
});

// Reducer 中使用 Adapter
on(ProductActions.loadProductsSuccess, (state, { products }) => {
  return productsAdapter.setAll(products, {
    ...state,
    loading: false,
    error: null
  });
}),

// 使用 Adapter 選擇器
export const {
  selectIds,
  selectEntities,
  selectAll,
  selectTotal
} = productsAdapter.getSelectors(selectProductState);
```

### NgRx 的優勢

1. **可預測性**：狀態變更遵循嚴格的模式，使應用程式行為更可預測
2. **集中管理**：所有狀態在一個地方，避免狀態散落各處
3. **追蹤性**：每個狀態變更都有明確的動作記錄
4. **可測試性**：容易編寫單元測試，測試覆蓋率高
5. **開發工具**：配合 Redux DevTools 提供強大的除錯能力，包括時間旅行功能
6. **效能優化**：通過記憶化選擇器和優化的變更偵測提高效能

### 常見使用場景

NgRx 特別適合以下情況：

1. **多元件共享狀態**：當多個元件需要訪問和修改相同的資料（如購物車、使用者偏好設定）
2. **複雜的狀態邏輯**：狀態更新邏輯複雜，需要可預測的管理方式（如表單管理、多步驟流程）
3. **中大型應用**：應用程式規模增長到一定程度，需要更結構化的狀態管理
4. **團隊協作**：大型團隊需要一致的狀態管理模式，減少程式碼衝突和溝通成本

### 結論

NgRx 提供了一套完整的狀態管理解決方案，雖然有一定的學習曲線和樣板程式碼，但它帶來的可預測性、可維護性和可擴展性，使它成為 Angular 應用程式中狀態管理的強大選擇。特別是對於中大型專案來說，NgRx 的投資是值得的。

記住 NgRx 的流程：`Component` → `Action` → `Reducer` → `State` → `Selector` → `Component`，這個單向資料流是 NgRx 最核心的設計理念。

