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.

Por que isso para de renderizar no OnPushts
@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 causaA correção OnPush-limpa
Mutar um array ou objeto no lugarEscreva um signal (ou atribua uma referência nova)
Um valor setado de um callback não-AngularMova o valor pra um signal, ou markForCheck()
Um pai mutando um objeto de @InputPasse uma referência nova do pai
Você precisa mesmo de checagem a cada tickMantenha ChangeDetectionStrategy.Eager, de propósito
O mesmo componente, OnPush-limpots
@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.Eager que o ng update adicionou: esse é seu backlog.
  • Troque mutação no lugar por uma escrita de signal ou uma referência nova.
  • Pra valores setados de callbacks não-Angular, mova pra um signal ou chame markForCheck().
  • 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 Eager quando 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

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.

Abrir playbook →

André Ramosdisponível para vagas remotas, UTC−3Entre em contato →

Leia também

Guia · 11 min

Angular 22: o que mudou de verdade, e o que fazer num app em produção

Um guia sênior, focado em produção, do Angular 22: o default OnPush, os Signal Forms e as APIs de async signal estabilizados, as migrações de teste, as viradas de default menores, e um plano deliberado pra adotar.

Ler artigo →

Guia · 10 min

O código que quebra quando Angular fica zoneless

Um guia técnico para revisar timers, subscriptions, forms, callbacks de terceiros e testes antes de um spike zoneless no Angular.

Ler artigo →

Nota · 6 min

O que adotar, testar ou deixar para depois no Angular 21

Um filtro de adoção para Angular 21: o que entra em trechos que já vão mudar, o que merece branch e o que deve ficar longe de fluxos críticos.

Ler artigo →