Modern Angular
OnPush é o default no Angular 22: o que quebra e o que fazer
O Angular 22 virou o default de change detection para OnPush. Um componente sem estratégia changeDetection explícita agora é OnPush, não eager, e o ng update escreve ChangeDetectionStrategy.Eager nos seus componentes existentes pra mantê-los funcionando. Isso preserva o comportamento, mas também te entrega uma lista precisa dos componentes que vale arrumar.
O que mudou, em uma linha
Antes do v22, um componente que nunca setou uma estratégia changeDetection rodava eager: o Angular o checava a cada tick. No Angular 22 esse mesmo componente é OnPush, que só re-renderiza quando uma referência de input muda, um evento dispara dentro dele, um async pipe emite, ou um signal que ele lê é escrito.
O ng update não quebra seu app. Ele adiciona changeDetection: ChangeDetectionStrategy.Eager nos seus componentes existentes pra nada mudar em runtime. Componentes novos, porém, começam OnPush, e esse é o default certo. O trabalho interessante é decidir quais dos componentes migrados precisavam mesmo da checagem eager, e quais só nunca tinham sido marcados como OnPush porque ninguém parou pra mexer.
O que de fato quebra
Tire o Eager que a migração pôs de um componente e a falha tem sempre o mesmo formato: o dado mudou, mas a view não. O OnPush não checa a cada tick, então qualquer estado que você muta sem avisar o Angular fica invisível.
Os suspeitos de sempre são mutação no lugar (dar push num array ou setar uma propriedade num objeto que o template lê), um valor setado a partir de um callback que o Angular não conhece (um widget de terceiros, um addEventListener cru, um setTimeout que escreve um campo), e um pai que muta um objeto de @Input em vez de passar uma referência nova. Nenhum deles marca a view como suja, então o OnPush nunca re-renderiza.
@Component({ selector: 'app-cart', template: '{{ items.length }} items' })
export class CartComponent {
items: Item[] = [];
// Mutar no lugar NÃO marca a view OnPush como suja: a contagem fica velha.
add(item: Item) {
this.items.push(item);
}
}Os dois jeitos de fazer certo
Existe a saída de emergência e a correção. A saída é o Eager que a migração já escreveu: mantém o componente checando a cada tick, o que serve de paliativo mas é o comportamento que você está tentando deixar pra trás. A correção é dar ao componente um motivo de re-renderizar que o OnPush entenda.
A correção mais limpa é signals, porque escrever um signal marca a view OnPush como suja pra você. Onde signals ainda não encaixam, uma referência nova num @Input funciona, e em último caso você injeta ChangeDetectorRef e chama markForCheck() depois da mudança. Use o markForCheck sabendo que ele é sinal de que aquele estado provavelmente deveria ser um signal.
| A causa | A correção OnPush-limpa |
|---|---|
| Mutar um array ou objeto no lugar | Escreva um signal (ou atribua uma referência nova) |
| Um valor setado de um callback não-Angular | Mova o valor pra um signal, ou markForCheck() |
Um pai mutando um objeto de @Input | Passe uma referência nova do pai |
| Você precisa mesmo de checagem a cada tick | Mantenha ChangeDetectionStrategy.Eager, de propósito |
@Component({ selector: 'app-cart', template: '{{ items().length }} items' })
export class CartComponent {
readonly items = signal<Item[]>([]);
// Uma escrita de signal marca a view OnPush como suja. A contagem atualiza.
add(item: Item) {
this.items.update((list) => [...list, item]);
}
}Como achar os componentes, inclusive em testes
Você não precisa caçar. Todo ChangeDetectionStrategy.Eager que a migração adicionou é um componente que rodava eager, e cada um é candidato a virar OnPush de verdade. Trabalhe essa lista por custo: os baratos viram signals, os que sustentam muita coisa ficam com Eager até você ter tempo, e você apaga a linha quando o componente está limpo.
Os testes mostram o mesmo formato. Um componente OnPush não renderiza só porque o tempo passou ou o estado mudou; você chama fixture.detectChanges() depois da mudança pra ver. É a mesma disciplina que os testes de tempo no Vitest pedem, e o mesmo tipo de dependência escondida com que um app tropeça quando vira totalmente zoneless.
O que eu faria
Deixe os componentes novos serem OnPush, porque o default do v22 é o certo. Trate os Eager que a migração adicionou como backlog, não como veredito: converta os fáceis pra signals conforme você toca no código, e deixe o resto com Eager até haver motivo pra voltar. Não há campanha aqui, só um default que finalmente aponta pro lado certo.
Isso é uma fatia do upgrade pro Angular 22. O default OnPush é a mudança que mexe com mais componentes, e ela recompensa o mesmo hábito que o resto do Angular moderno: deixar as mudanças de estado visíveis pro framework em vez de torcer pra um tick pegar.
Artefato para reaproveitar
Deixando um componente OnPush-limpo
- Ache as linhas
ChangeDetectionStrategy.Eagerque ong updateadicionou: esse é seu backlog. - Troque mutação no lugar por uma escrita de
signalou uma referência nova. - Pra valores setados de callbacks não-Angular, mova pra um
signalou chamemarkForCheck(). - Garanta que os pais passem uma referência nova em vez de mutar um objeto de
@Input. - Em testes, chame
fixture.detectChanges()depois de uma mudança pra a view OnPush renderizar. - Apague a linha
Eagerquando o componente re-renderiza sem ela; mantenha só onde a checagem a cada tick é mesmo necessária.
Perguntas frequentes
Por que meu componente Angular parou de atualizar depois de subir pro v22?
Porque OnPush é o default de change detection no Angular 22. Um componente sem estratégia changeDetection explícita agora só re-renderiza numa mudança de referência de input, num evento, numa emissão de async pipe, ou numa escrita de signal. Se você muta estado no lugar ou o seta de um callback que o Angular não conhece, a view não atualiza. Mova o estado pra um signal, passe uma referência nova, ou chame markForCheck().
O que significa ChangeDetectionStrategy.Eager no Angular 22?
É o comportamento default antigo: o componente é checado a cada tick de change detection. Como o v22 faz do OnPush o default, o ng update adiciona ChangeDetectionStrategy.Eager nos seus componentes existentes pra o comportamento não mudar. É um paliativo seguro, e cada um é candidato a virar OnPush.
Preciso migrar todos os componentes pra OnPush?
Não. A migração os mantém funcionando marcando como Eager. Você os converte no seu ritmo, geralmente movendo o estado mutado pra signals, e mantém Eager nos poucos componentes que precisam mesmo de checagem a cada tick.
Por que meu componente OnPush não atualiza num teste?
Um componente OnPush re-renderiza só quando algo marca a view como suja. Num teste, chame fixture.detectChanges() depois da mudança pra a view refletir. Avançar o tempo ou setar estado sozinho não renderiza um componente OnPush.
Fontes consultadas
- https://angular.dev/events/v22
- https://angular.dev/best-practices/runtime-performance
- https://angular.dev/guide/zoneless
- https://angular.dev/guide/signals
- https://blog.ninja-squad.com/2026/06/03/what-is-new-angular-22.0
Modern Angular Playbook
Este artigo é um play.
O Playbook do Angular Moderno reúne o diagnóstico, a matriz de adoção, onze jogadas e o plano de 30 dias. Grátis, em inglês e português.
Você recebe os dois PDFs por email, pela lista do Dojo IA.
André Ramosdisponível para vagas remotas, UTC−3Entre em contato →