Especialização em Front-End com Angular • Demonstrações Práticas Compiláveis


💻 Código de Exemplo 17 — Estado Global Moderno com NgRx SignalStore

Este exemplo apresenta a criação e consumo de uma Store de estado corporativa no Angular 18+ utilizando o NgRx SignalStore (@ngrx/signals), combinando withState, withComputed, withMethods e patchState de forma totalmente tipada e sem o boilerplate clássico de actions/reducers/effects.

1. Definição da Store: src/app/stores/order.store.ts

// src/app/stores/order.store.ts
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
import { computed } from '@angular/core';

export interface OrderItem {
  id: string;
  customer: string;
  amount: number;
  status: 'PENDING' | 'PAID' | 'CANCELED';
}

interface OrderState {
  orders: OrderItem[];
  filterStatus: 'ALL' | 'PENDING' | 'PAID';
  isLoading: boolean;
}

const initialState: OrderState = {
  orders: [
    { id: 'ord-101', customer: 'Banco Horizonte Tech', amount: 15400.0, status: 'PAID' },
    { id: 'ord-102', customer: 'Varejo Global Brasil', amount: 4800.0, status: 'PENDING' },
    { id: 'ord-103', customer: 'Startup Nexus Cloud', amount: 9200.0, status: 'PAID' }
  ],
  filterStatus: 'ALL',
  isLoading: false
};

export const OrderStore = signalStore(
  { providedIn: 'root' }, // Injeção Singleton Global
  withState(initialState),

  // 1. Getters Computados Tipados Reativos
  withComputed(({ orders, filterStatus }) => ({
    filteredOrders: computed(() => {
      const list = orders();
      const filter = filterStatus();
      if (filter === 'ALL') return list;
      return list.filter((item) => item.status === filter);
    }),
    totalRevenue: computed(() => {
      return orders()
        .filter((i) => i.status === 'PAID')
        .reduce((sum, item) => sum + item.amount, 0);
    }),
    pendingCount: computed(() => {
      return orders().filter((i) => i.status === 'PENDING').length;
    })
  })),

  // 2. Métodos e Ações de Mutação de Estado com patchState
  withMethods((store) => ({
    setFilter(status: 'ALL' | 'PENDING' | 'PAID'): void {
      patchState(store, { filterStatus: status });
    },

    addOrder(customer: string, amount: number): void {
      const newOrder: OrderItem = {
        id: `ord-${Date.now()}`,
        customer,
        amount,
        status: 'PENDING'
      };
      patchState(store, (state) => ({
        orders: [newOrder, ...state.orders]
      }));
    },

    markAsPaid(orderId: string): void {
      patchState(store, (state) => ({
        orders: state.orders.map((o) => (o.id === orderId ? { ...o, status: 'PAID' } : o))
      }));
    }
  }))
);

2. Componente Consumidor: src/app/components/order-dashboard.component.ts

// src/app/components/order-dashboard.component.ts
import { Component, inject } from '@angular/core';
import { CommonModule, CurrencyPipe } from '@angular/common';
import { OrderStore } from '../stores/order.store';

@Component({
  selector: 'app-order-dashboard',
  standalone: true,
  imports: [CommonModule, CurrencyPipe],
  template: `
    <div class="max-w-2xl mx-auto p-6 bg-white border border-slate-200 rounded-2xl shadow-sm space-y-6">
      <div class="flex items-center justify-between border-b pb-4">
        <div>
          <h2 class="text-xl font-bold text-slate-800">📊 Gestão de Pedidos (NgRx SignalStore)</h2>
          <p class="text-xs text-slate-500">Estado centralizado, modular e sem boilerplate de Reducers</p>
        </div>
        <button
          type="button"
          (click)="simulateNewOrder()"
          class="px-3.5 py-1.5 text-xs font-semibold bg-red-600 hover:bg-red-700 text-white rounded-lg"
        >
          + Novo Pedido
        </button>
      </div>

      <!-- Cards de KPIs Computados da Store -->
      <div class="grid grid-cols-2 gap-4">
        <div class="p-4 bg-slate-50 border rounded-xl">
          <span class="text-[10px] uppercase font-bold text-slate-400">Receita Paga (totalRevenue)</span>
          <div class="text-2xl font-black text-emerald-600 mt-1">
            {{ store.totalRevenue() | currency:'BRL':'symbol':'1.2-2' }}
          </div>
        </div>

        <div class="p-4 bg-slate-50 border rounded-xl">
          <span class="text-[10px] uppercase font-bold text-slate-400">Pedidos Pendentes (pendingCount)</span>
          <div class="text-2xl font-black text-amber-600 mt-1">
            {{ store.pendingCount() }} ordens
          </div>
        </div>
      </div>

      <!-- Filtro com Método da Store -->
      <div class="flex gap-2">
        <button
          type="button"
          (click)="store.setFilter('ALL')"
          [class]="store.filterStatus() === 'ALL' ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-700'"
          class="px-3 py-1.5 text-xs font-semibold rounded-lg"
        >
          Todos ({{ store.orders().length }})
        </button>
        <button
          type="button"
          (click)="store.setFilter('PENDING')"
          [class]="store.filterStatus() === 'PENDING' ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-700'"
          class="px-3 py-1.5 text-xs font-semibold rounded-lg"
        >
          Pendentes
        </button>
        <button
          type="button"
          (click)="store.setFilter('PAID')"
          [class]="store.filterStatus() === 'PAID' ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-700'"
          class="px-3 py-1.5 text-xs font-semibold rounded-lg"
        >
          Pagos
        </button>
      </div>

      <!-- Tabela Reativa -->
      <div class="border rounded-xl divide-y overflow-hidden text-xs">
        @for (order of store.filteredOrders(); track order.id) {
          <div class="p-3.5 flex justify-between items-center hover:bg-slate-50">
            <div>
              <span class="font-bold text-slate-800">{{ order.customer }}</span>
              <p class="text-slate-400 font-mono text-[10px]">{{ order.id }}</p>
            </div>
            <div class="flex items-center gap-3">
              <span class="font-mono font-bold text-slate-900">
                {{ order.amount | currency:'BRL':'symbol':'1.2-2' }}
              </span>
              @if (order.status === 'PENDING') {
                <button
                  type="button"
                  (click)="store.markAsPaid(order.id)"
                  class="text-[11px] px-2.5 py-1 bg-emerald-50 text-emerald-700 rounded-md font-semibold hover:bg-emerald-100"
                >
                  Pagar
                </button>
              } @else {
                <span class="text-[10px] font-bold px-2 py-0.5 rounded bg-emerald-100 text-emerald-800">
                  PAGO
                </span>
              }
            </div>
          </div>
        }
      </div>
    </div>
  `
})
export class OrderDashboardComponent {
  // Injeção da SignalStore
  readonly store = inject(OrderStore);

  simulateNewOrder(): void {
    const clients = ['Logística Delta', 'Fintech Inovação', 'Cloud Systems'];
    const client = clients[Math.floor(Math.random() * clients.length)];
    const amount = Math.floor(Math.random() * 8000) + 1200;
    this.store.addOrder(client, amount);
  }
}

🖥️ Saída / Comportamento Esperado

Renderização Inicial:
Receita Paga: R$ 24.600,00 (Soma de ord-101 e ord-103)
Pedidos Pendentes: 1 ordem (ord-102)

Interação 1 — Clicar em "Pagar" na ordem ord-102:
store.markAsPaid('ord-102') dispara patchState.
O status muda para 'PAGO'.
Receita Paga sobe automaticamente para R$ 29.400,00.
Pedidos Pendentes cai para 0 ordens.

Interação 2 — Clicar em "+ Novo Pedido":
store.addOrder() adiciona uma nova linha no topo.
Pedidos Pendentes sobe para 1 ordem.
Todos os botões de filtro e contadores reagem instantaneamente.

🔍 Destaques da Implementação

  1. Zero Reducers e Zero Actions Strings: O NgRx SignalStore traz toda a robustez de arquitetura do NgRx sem a sobrecarga de arquivos duplicados.
  2. patchState Seguro e Imutável: Garante atualizações atômicas de estado sem mutar o objeto anterior diretamente.
  3. Signals Nativos: Propriedades de estado e getters computados são Signals (store.totalRevenue()), integrando perfeitamente com @for e templates modernos.