NgRx consegue travar a velocidade de sprint de um time pequeno por meses. Isso é um padrão de falha, não o postmortem de um time específico, e sempre tem o mesmo formato: poucos desenvolvedores, prazos apertados, e alguém propondo colocar cada pedaço de estado no NgRx enquanto todo mundo concorda. Os primeiros sprints vão embora escrevendo actions, reducers, selectors e effects para um estado que poderia ter vivido em dois services. Depois, adicionar um campo a um slice significa editar a action, a interface de estado, o reducer, o selector e o componente que lê aquilo. No fim das contas alguém recorre a meta-reducers para gerenciar a complexidade introduzida para gerenciar a complexidade. A resposta raramente é arrancar o NgRx. Ele ganha seu lugar para o estado que de fato cruza features. O que precisa mudar é o padrão de colocar tudo no store.
O Que NgRx Realmente Resolve
O pitch honesto do NgRx não é experiência do desenvolvedor. É auditabilidade e coordenação de estado entre features. Quando você tem estado que é lido e mutado por componentes em diferentes feature modules com lazy-loading, quando você precisa do Redux DevTools com time-travel para debugar problemas de estado em produção, quando múltiplos times precisam de contratos explícitos sobre como transições de estado acontecem, é aí que o NgRx justifica sua cerimônia.
No BMO, essas condições eram reais. Trabalhei na plataforma digital de solicitação de empréstimos do banco, que é construída como micro-frontends. O NgRx já estava lá quando entrei, e as decisões de arquitetura pertencem a um time separado. Tendo trabalhado dentro dele, acho que é a escolha certa para esse tipo de produto: em uma aplicação financeira grande, um service com um BehaviorSubject se tornaria impossível de debugar em seis meses. O NgRx torna as transições explícitas e rastreáveis.
O Padrão Clássico, Sem Enfeitar
Antes de recorrer ao Signal Store ou qualquer alternativa mais leve, entenda o que você está decidindo não usar. O padrão clássico Actions, Reducers, Selectors, Effects é verboso por design. A verbosidade é a documentação. Aqui está um slice ilustrativo para uma submissão genérica de solicitação de empréstimo, escrito para este post e não tirado de nenhuma base de código real:
// actions/application.actions.ts
export const submitApplication = createAction('[Application] Submit');
export const submitApplicationSuccess = createAction(
'[Application] Submit Success',
props<{ applicationId: string; offerAmount: number }>()
);
export const submitApplicationFailure = createAction(
'[Application] Submit Failure',
props<{ error: string; retryable: boolean }>()
);
// reducers/application.reducer.ts
export interface ApplicationState {
applicationId: string | null;
offerAmount: number | null;
submitting: boolean;
error: string | null;
retryable: boolean;
}
const initialState: ApplicationState = {
applicationId: null,
offerAmount: null,
submitting: false,
error: null,
retryable: false
};
export const applicationReducer = createReducer(
initialState,
on(submitApplication, state => ({ ...state, submitting: true, error: null })),
on(submitApplicationSuccess, (state, { applicationId, offerAmount }) => ({
...state, applicationId, offerAmount, submitting: false
})),
on(submitApplicationFailure, (state, { error, retryable }) => ({
...state, error, retryable, submitting: false
}))
);// effects/application.effects.ts
@Injectable()
export class ApplicationEffects {
private actions$ = inject(Actions);
private applicationService = inject(ApplicationService);
submit$ = createEffect(() =>
this.actions$.pipe(
ofType(submitApplication),
exhaustMap(() =>
this.applicationService.submit().pipe(
map(result => submitApplicationSuccess(result)),
catchError(err => of(submitApplicationFailure({
error: err.message,
retryable: err.status >= 500
})))
)
)
)
);
}Eu uso exhaustMap aqui, não switchMap. Em um envio de solicitação de empréstimo, você não cancela requisições em andamento quando o usuário dá duplo clique. Esse é o tipo de decisão que se torna explícita com Effects e invisível com um método de service.
Quando Pular o Padrão Clássico
- Estado local de uma feature que nunca é lido fora dela
- Carregamento simples de dados assíncronos sem dependências entre features
- Projetos solo ou de times pequenos onde a trilha de auditoria do Redux DevTools não agrega valor
- Fases de protótipo e MVP onde você não pode arcar com o custo do boilerplate
- Estado que muda em resposta a input do usuário dentro de um único formulário ou passo de wizard
Para todos esses casos, um service com signals é a escolha certa hoje. Em versões do Angular anteriores à 16, onde signals não são uma opção, a mesma jogada funciona com services simples e BehaviorSubjects: tire do store o estado no formato de wizard e mantenha o NgRx apenas para o estado que genuinamente cruza features. Centenas de linhas de actions, reducers e selectors podem colapsar em dois services pequenos, mais simples de testar, mais simples de debugar e mais rápidos de mudar.
NgRx Signal Store: O Meio-Termo
O NgRx Signal Store saiu como developer preview no NgRx v17 e ficou pronto para produção no NgRx v18. Repare que essas são versões do NgRx, não do Angular. Ele é genuinamente bom. Ele te dá o gerenciamento de estado estruturado e explícito do NgRx com signals em vez de observables RxJS, e sem a cerimônia de Actions, Reducers, Effects para casos que não precisam.
import { computed, inject } from '@angular/core';
import { tapResponse } from '@ngrx/operators';
import { signalStore, withState, withMethods, withComputed, patchState } from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { pipe, switchMap, tap } from 'rxjs';
import { DocumentService, UploadedDocument } from './document.service';
interface DocumentUploadState {
applicationId: string | null;
documents: UploadedDocument[];
uploading: boolean;
error: string | null;
}
export const DocumentUploadStore = signalStore(
{ providedIn: 'root' },
withState<DocumentUploadState>({
applicationId: null,
documents: [],
uploading: false,
error: null
}),
withComputed(({ documents }) => ({
documentCount: computed(() => documents().length),
hasRequiredDocuments: computed(() => documents().length >= 2)
})),
withMethods((store, service = inject(DocumentService)) => ({
uploadDocument: rxMethod<File>(pipe(
tap(() => patchState(store, { uploading: true })),
switchMap(file =>
service.upload(file).pipe(
tapResponse(
doc => patchState(store, { documents: [...store.documents(), doc], uploading: false }),
err => patchState(store, { error: err.message, uploading: false })
)
)
)
))
}))
);O tapResponse saiu do @ngrx/component-store e foi para o @ngrx/operators no NgRx 18, que é o import que você quer em qualquer versão atual. Eu uso Signal Store para estado de feature que é complexo demais para um service puro mas não precisa de auditoria pelo Redux DevTools. O threshold é mais ou menos: múltiplas operações assíncronas com estados de carregamento interdependentes, ou estado compartilhado entre três ou mais componentes dentro da mesma feature.
A Decisão Que Eu Realmente Uso
- Estado local do componente: signals diretamente no componente
- Estado de feature com complexidade moderada: NgRx Signal Store
- Estado global com leituras entre features, orquestração assíncrona complexa ou requisitos de auditoria: NgRx clássico
- Se você herdou NgRx clássico: mantenha, não reescreva, os padrões são sólidos
NgRx não torna sua aplicação mais profissional. Ele torna as transições de estado explícitas. Isso só tem valor se você estava perdendo o controle delas.
O erro que eu mais vejo é times adotando NgRx porque transmite seriedade, não porque resolve um problema que eles têm. Se você nunca enviou um bug causado por estado compartilhado imprevisível entre features, provavelmente não precisa do NgRx clássico ainda. Construa com signals e Signal Store. Adicione toda a maquinaria Redux quando a complexidade realmente exigir.
Referências
- NgRx official documentation. https://ngrx.io/docs
- NgRx Signal Store guide. https://ngrx.io/guide/signals/signal-store
- NgRx Effects. https://ngrx.io/guide/effects
- NgRx MockStore for testing. https://ngrx.io/guide/store/testing