Capítulo 17: Gerenciamento de Estado Corporativo com NgRx SignalStore
Capítulo 17: Gerenciamento de Estado Corporativo com NgRx SignalStore
Especialização em Front-End com Angular • Angular 18+ • Signals, Standalone Architecture, RxJS, NgRx e SSR
🗺️ Mapa Conceitual da Arquitetura do NgRx SignalStore
flowchart TD
Comp["Componente Consumidor"] -->|"Invocação de Método / Dispatch"| Methods["withMethods() (Ações & rxMethod)"]
subgraph SIGNAL_STORE["NgRx SignalStore"]
State["withState() (Estado Reativo Centralizado)"]
Computed["withComputed() (Seletores Memoizados)"]
Methods
Hooks["withHooks() (onInit / onDestroy)"]
end
Methods -->|"patchState()"| State
State --> Computed
Computed -->|"Signals Nativos"| Comp
State -->|"Signals Nativos"| Comp
Methods -->|"Chamada HTTP Externa"| Backend["API REST"]
Backend -->|"Retorno de Dados"| Methods
style Comp fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style SIGNAL_STORE fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style Backend fill:#fff3e0,stroke:#ff9800,stroke-width:2px
🏛️ 1. O Fim do Boilerplate Redux: A Chegada do SignalStore
Durante quase uma década, aplicações corporativas em Angular recorreram ao padrão Redux (NgRx Store clássico), exigindo a escrita de dezenas de arquivos complementares para uma única funcionalidade: actions.ts, reducers.ts, effects.ts, selectors.ts e facades.ts.
Com a introdução do NgRx SignalStore (membro oficial da suíte @ngrx/signals):
- O estado global ou modular é declarado de forma funcional, tipada e extensível.
- O consumo pelos componentes ocorre diretamente via Signals nativos, sem a necessidade de pipes assíncronos (
AsyncPipe). - Atualizações atômicas e imutáveis são feitas com a função utilitária
patchState(). - O código de gerenciamento de estado é reduzido em até 75% de linhas, mantendo previsibilidade e manutenibilidade rigorosas.
⚙️ 2. Construindo uma Store Completa com signalStore
Veja a implementação de uma store corporativa para gestão de faturas financeiras:
// stores/faturas.store.ts
import { inject } from '@angular/core';
import { signalStore, withState, withComputed, withMethods, withHooks, patchState } from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { computed } from '@angular/core';
import { pipe, switchMap, tap } from 'rxjs';
import { HttpClient } from '@angular/common/http';
export interface Fatura {
id: string;
cliente: string;
valor: number;
status: 'PENDENTE' | 'PAGO' | 'ATRASADO';
}
interface FaturasState {
faturas: Fatura[];
carregando: boolean;
filtroStatus: 'TODOS' | 'PENDENTE' | 'PAGO' | 'ATRASADO';
termoBusca: string;
}
const estadoInicial: FaturasState = {
faturas: [],
carregando: false,
filtroStatus: 'TODOS',
termoBusca: ''
};
export const FaturasStore = signalStore(
{ providedIn: 'root' }, // Injeção Singleton Global
withState(estadoInicial),
// Seletores derivados reativos (Computed Signals)
withComputed(({ faturas, filtroStatus, termoBusca }) => ({
faturasFiltradas: computed(() => {
const lista = faturas();
const status = filtroStatus();
const busca = termoBusca().toLowerCase();
return lista.filter(f => {
const matchesStatus = status === 'TODOS' || f.status === status;
const matchesBusca = f.cliente.toLowerCase().includes(busca);
return matchesStatus && matchesBusca;
});
}),
totalPendente: computed(() => {
return faturas()
.filter(f => f.status === 'PENDENTE' || f.status === 'ATRASADO')
.reduce((acc, f) => acc + f.valor, 0);
})
})),
// Métodos de mutação síncrona e orquestração assíncrona
withMethods((store, http = inject(HttpClient)) => ({
definirFiltroStatus(filtroStatus: FaturasState['filtroStatus']): void {
patchState(store, { filtroStatus });
},
definirTermoBusca(termoBusca: string): void {
patchState(store, { termoBusca });
},
// Método assíncrono conectado a RxJS para buscar dados do backend
carregarFaturas: rxMethod<void>(
pipe(
tap(() => patchState(store, { carregando: true })),
switchMap(() => http.get<Fatura[]>('/api/faturas')),
tap(faturas => patchState(store, { faturas, carregando: false }))
)
)
})),
// Ciclo de vida da Store
withHooks({
onInit(store) {
console.log('FaturasStore inicializada. Carregando dados...');
store.carregarFaturas();
}
})
);
💻 3. Consumo Reativo no Componente Standalone
No componente, consumir o SignalStore é tão simples quanto injetar um serviço tradicional. Todas as fatias de estado e seletores computados já chegam prontos como Signals:
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FaturasStore } from './stores/faturas.store';
@Component({
selector: 'app-gestao-faturas',
standalone: true,
imports: [CommonModule],
template: `
<div class="painel">
<header>
<h2>Faturamento da Empresa</h2>
<p>Total em Aberto: {{ store.totalPendente() | currency:'BRL' }}</p>
</header>
<div class="controles">
<button (click)="store.definirFiltroStatus('TODOS')">Todas</button>
<button (click)="store.definirFiltroStatus('PENDENTE')">Pendentes</button>
<button (click)="store.definirFiltroStatus('PAGO')">Pagas</button>
</div>
@if (store.carregando()) {
<p>Atualizando faturas...</p>
} @else {
<ul>
@for (fatura of store.faturasFiltradas(); track fatura.id) {
<li>
<span>{{ fatura.cliente }}</span> -
<strong>{{ fatura.valor | currency:'BRL' }}</strong>
<span>({{ fatura.status }})</span>
</li>
} @empty {
<li>Nenhuma fatura encontrada com os filtros atuais.</li>
}
</ul>
}
</div>
`
})
export class GestaoFaturasComponent {
readonly store = inject(FaturasStore);
}
🧩 4. Criação de Recursos Reutilizáveis com signalStoreFeature
Você pode modularizar sua arquitetura criando extensões reutilizáveis (por exemplo, um feature store genérico de paginação ou controle de requisição HTTP):
import { signalStoreFeature, withState, withComputed } from '@ngrx/signals';
import { computed } from '@angular/core';
export function withPagination(pageSizePadrao = 10) {
return signalStoreFeature(
withState({ paginaAtual: 1, itensPorPagina: pageSizePadrao, totalRegistros: 0 }),
withComputed(({ totalRegistros, itensPorPagina }) => ({
totalPaginas: computed(() => Math.ceil(totalRegistros() / itensPorPagina()))
}))
);
}
// Compondo no store final:
// signalStore(withState(...), withPagination(20), withMethods(...))
⚖️ 5. Comparativo: NgRx Clássico vs SignalStore
| Aspecto | NgRx Clássico (Actions / Reducers) | NgRx SignalStore Moderno |
|---|---|---|
| Linhas de Código (Boilerplate) | Elevadíssimo (4 a 5 arquivos por entidade) | Mínimo (1 arquivo conciso e modular) |
| Tipo de Reatividade | Observables de RxJS (store.select(...)) |
Signals Nativos (store.meuDado()) |
| Integração com Template | Requer | async em quase todas as tags |
Chamada direta sem overhead |
| Escopo de Injeção | Quase exclusivamente Root Global | Suporta Root Global ou Escopo Local por Componente |
| Curva de Aprendizado | Íngreme | Suave e intuitiva |
⚠️ 6. Armadilhas Comuns e Anti-Patterns
- Mutação Direta de Arrays no
patchState:- Erro:
patchState(store, state => { state.faturas.push(novo); return state; }). - Correção: Mantenha imutabilidade:
patchState(store, state => ({ faturas: [...state.faturas, novo] })).
- Erro:
- Transformar Todo Estado Local em Global:
- Não coloque variáveis temporárias da view (como se um dropdown está aberto) em stores globais. Use
signal()local no componente.
- Não coloque variáveis temporárias da view (como se um dropdown está aberto) em stores globais. Use
🛡️ 7. Boas Práticas e Arquitetura Limpa
- Stores de Feature vs Stores Globais: Registre stores que atendem telas inteiras no array
providers: [MinhaFeatureStore]do componente pai ou na rota, garantindo que o estado seja limpo automaticamente quando o usuário sair da página.
📋 8. Cheat-Sheet: Resumo Executivo
export const MinhaStore = signalStore(
withState({ contador: 0 }),
withComputed(({ contador }) => ({ dobro: computed(() => contador() * 2) })),
withMethods(store => ({
incrementar() { patchState(store, s => ({ contador: s.contador + 1 })); }
}))
);