💻 Exemplos: Capítulo 17: Gerenciamento de Estado com NgRx SignalStore
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
- Zero Reducers e Zero Actions Strings: O NgRx SignalStore traz toda a robustez de arquitetura do NgRx sem a sobrecarga de arquivos duplicados.
patchStateSeguro e Imutável: Garante atualizações atômicas de estado sem mutar o objeto anterior diretamente.- Signals Nativos: Propriedades de estado e getters computados são Signals (
store.totalRevenue()), integrando perfeitamente com@fore templates modernos.