Angular Signals: reatividade granular
Como os Signals funcionam por baixo do capô, do grafo de dependências ao push/pull preguiçoso, e por que isso transforma o change detection do Angular em bisturi no lugar de marreta.
Foi num sábado onde queria “paz”. Instalei o Angular 22 achando que seria só mais um ng update de rotina, rodei ng update @angular/core@22 @angular/cli@22, fui buscar um café e voltei esperando o de sempre: uns avisos de depreciação, um changelog gordo, nada que abalasse o meu dia. Não foi bem assim. O que essa atualização fez comigo foi de outra ordem. Ela pegou um monte de recurso que eu vinha tratando como “experimento promissor”, cravou tudo como estável e, no caminho, mexeu em defaults que eu tinha esquecido que existiam e porm fim, me fez chorar em posição fetal.
Deixa eu te contar o que encontrei. Acho que vale mais como relato de quem atravessou a migração do que como listinha de novidades.
Se eu tivesse que resumir a v22 numa frase, seria essa: ela parou de adicionar recursos e passou a assinar compromissos. Um ano atrás, na 21, o time tinha jogado três apostas grandes na mesa em modo preview, o Signal Forms, o Angular Aria e as APIs de reatividade assíncrona com resource. Brinquei com as três, gostei, e guardei todas na gaveta, com aquele receio saudável de quem já se queimou botando developer preview em produção. Pois é. Na 22 elas graduaram. Todas estáveis, de uma vez.
Signal Forms foi a que mais me pegou. Quem já apanhou de FormGroup, FormControl e da dança de subscribe e unsubscribe do Reactive Forms sabe exatamente da dor que eu falo. A proposta nova vira a lógica do avesso: os dados do formulário passam a morar num signal que é seu, e o próprio Angular deriva dali a estrutura de validação e o binding.
import { signal } from "@angular/core";
import { form, required } from "@angular/forms/signals";
const pagamento = signal({ tipo: "", valor: 0 });
const f = form(pagamento, (schema) => {
required(schema.tipo, { message: "Campo obrigatório" });
});
Cadê o FormBuilder? Não tem. E a subscription pronta pra vazar? Também não. O estado é um signal, a validação é declarativa, e a coisa toda se encaixa no mesmo modelo reativo que eu já usava pro resto da aplicação. Depois de anos tratando formulário como um universo à parte, com regras próprias e gambiarras próprias, ver ele finalmente falando a língua do meu estado foi um alívio genuíno.
O resource foi a segunda que me ganhou. É a resposta a uma pergunta que o RxJS sempre respondeu com cerimônia demais: como buscar dado remoto e expor loading, erro e valor sem escrever um BehaviorSubject na unha pra cada requisição?
import { httpResource } from "@angular/common/http";
const cidade = signal("Londrina");
const clima = httpResource(() => `/api/clima/${cidade()}`);
// clima.value(), clima.isLoading(), clima.error() são signals
Muda a cidade? A requisição refaz sozinha, porque o resource leu o signal ali dentro e sabe que depende dele. Sumiu o switchMap. Sumiu o takeUntilDestroyed. Evaporou aquele boilerplate que eu recopiava sem parar, arquivo após arquivo. Pra fetch simples, virou minha primeira escolha. O RxJS segue imbatível quando o assunto é orquestração pesada, com debounce, retry e cancelamento encadeados, mas o feijão com arroz do “busca o dado e mostra na tela” ficou trivial.
E aí vem uma mudança que ocupa duas linhas no changelog e vira o jogo na prática: o OnPush agora é o comportamento padrão. Criou componente sem declarar nada? Ele já nasce OnPush. E o velho ChangeDetectionStrategy.Default, aquele que varria a árvore inteira em busca de qualquer coisinha alterada, foi rebatizado de ChangeDetectionStrategy.Eager, um nome que enfim conta a verdade sobre o que ele faz. Junta com o zoneless, que já estava ligado por baixo desde a 21, e o recado não dava pra ser mais claro. A reatividade granular deixou de ser otimização opcional. Virou o chão de fábrica.
Traçando a linha entre as duas, eu diria que uma abriu as portas e a outra fechou o contrato. Foi na 21 que o zoneless assumiu o comando e os signals invadiram a API dos componentes de vez. Só que Signal Forms, Aria e resource ainda carregavam aquela plaquinha de “sujeito a mudança”. Empolgante e arriscado na mesma medida.
A safra nova tira a plaquinha da mesa. Não traz features estonteantes e inéditas. Traz maturidade. O ganho não está numa API que eu não tinha ontem, está na permissão de levar pra produção, com suporte oficial e promessa de estabilidade, aquilo que eu namorava de longe. Pra quem decide arquitetura, isso muda tudo. “Experimental” e “estável” são dois planetas na hora de assinar embaixo.
Teve também os agrados de sintaxe, que eu nem esperava e acabei adorando. O @switch no template aprendeu a casar múltiplos valores no mesmo bloco e ganhou checagem exaustiva com never, capaz de transformar um caso esquecido em erro de compilação.
@switch (status) { @case ('pendente') @case ('processando') {
<p class="badge-azul">Em andamento</p>
} @case ('enviado') {
<p class="badge-verde">A caminho</p>
} @default never; }
Amanhã eu adiciono um status novo na union e esqueço de tratar ele aqui? O compilador me puxa pela orelha antes de o bug vazar pro usuário. Detalhe minúsculo. Sossego enorme.
Chegamos no ponto que eu mais quero que você leve dessa conversa, justamente o que some no meio dos anúncios brilhantes. A 22 não conversa só com quem começa projeto do zero. Ela cutuca todo mundo que já tem app rodando em campo, e faz isso de um jeito que vai dar trabalho.
Começa pelo strictTemplates, que virou true por padrão. Se o seu projeto convivia com checagem de template frouxa, prepare o coração: a atualização acende uma fileira inteira de erros que já estavam ali, só que invisíveis. Some a isso o OnPush implícito. Código que dependia, sem nem saber, do change detection varrendo tudo a cada tique pode simplesmente parar de repintar a tela. Dá pra reverter declarando Eager, claro. Mas isso é curativo, não é cura.
O golpe mais pesado, na minha leitura, é a aposentadoria dos builders baseados em Webpack. Aposentadoria é palavra gentil demais: o @angular-devkit/build-angular e o @ngtools/webpack foram marcados pra morrer, e a rota apontada é o @angular/build rodando sobre esbuild. Num app enxuto, o transplante é quase indolor. Já num monstro com Webpack recheado de loaders customizados, plugins específicos e gambiarras empilhadas ao longo de anos, isso vira um épico separado no cronograma. Tenho um projeto de 2019 com um webpack.config.js que ninguém da equipe abre sem antes respirar fundo, e é bem esse tipo de coisa que a v22 vem cobrar. E olha que nem é só o seu código: é a cadeia inteira de bibliotecas e schematics que ainda presume Webpack por baixo e vai precisar correr atrás do prejuízo.
A lista não para, e cada linha dela derruba alguém. provideRoutes saiu de cena, o lugar é do provideRouter. ComponentFactoryResolver e ComponentFactory foram embora de vez, coisa que um bocado de lib antiga de componente dinâmico ainda invocava lá no fundo. O fullTemplateTypeCheck evaporou. O Node passou a pedir 22.22.3 ou 24.15.0 pra cima (sim, esse .22.3 no meio faz diferença), e o TypeScript deu um salto pra 6.0, então o seu CI vai espernear antes mesmo de tentar compilar. E no SSR, a hidratação incremental assumiu a dianteira, ótimo pra performance e péssimo pra quem carregava suposições implícitas sobre a hora exata em que cada pedaço da página ganhava vida.
Lendo tudo isso de enfiada, uma coisa me saltou aos olhos. O Angular está empurrando toda a comunidade pra frente de propósito, e não está pedindo licença pra ninguém. Cada uma dessas aposentadorias é um recado seco às bibliotecas de terceiros: modernizem, ou fiquem pra trás. Incomoda no curto prazo, e eu entendo bem a bronca de quem vai queimar um trimestre nessa migração. Mas é o mesmíssimo movimento que enterrou os NgModules e nos entregou o standalone. E olha onde a gente chegou. Posso estar romantizando, admito. Quem passar o mês inteiro corrigindo template quebrado dificilmente vai achar isso poético.
Teve ainda uma frente que eu não esperava enxergar num anúncio de versão: a agêntica. O Angular MCP ganhou ferramentas voltadas ao servidor de desenvolvimento, tipo devserver.wait_for_build, devserver.start e devserver.stop, que deixam um agente de IA disparar o build, ler o erro que voltou e se corrigir sozinho, sem mim no meio do caminho. Chegou também o WebMCP, ainda experimental. Ele expõe ao navegador ferramentas estruturadas da própria aplicação, pra que agentes as acionem direto, e chega a montar esses utilitários a partir dos próprios Signal Forms. Não sei ainda quanto disso vai virar rotina no meu dia. O sinal, porém, é inconfundível: o time está desenhando a plataforma partindo do princípio de que boa parte do código vai passar pela mão de uma IA antes de encostar na minha.
Fechei o notebook no fim daquele sábado com uma sensação rara depois de um major. A de que a versão não me deu brinquedo novo, me deu chão firme. A pilha de signals fechou o ciclo. Estado, formulário, dado assíncrono e change detection, quatro mundos que viviam cada um com suas próprias regras, hoje conversam no mesmo idioma. O preço? Uma migração que não sai de graça e uma lista de aposentadorias que vai arder em quem empurrou a modernização com a barriga. Só que, se você já vinha apostando as fichas nos signals como eu apostei, a v22 chega menos como surpresa e mais como confirmação. Era pra ter sido assim desde o começo.