Angular Signals: Guia Completo de Reatividade e Estado 2024
Lembra da última vez que você debugou uma cadeia complexa de RxJS com múltiplas subscrições, async pipes por todo lado, e aquele memory leak que…

Lembra da última vez que você debugou uma cadeia complexa de RxJS com múltiplas subscrições, async pipes por todo lado, e aquele memory leak que você não conseguia rastrear? Ou quando teve que explicar para um dev júnior por que precisa fazer unsubscribe dos observables??
Hoje, quero compartilhar Angular Signals - um novo primitivo reativo que muda fundamentalmente como lidamos com estado em aplicações Angular. No final deste artigo, você vai entender como aproveitar signals para ter programação reativa mais limpa e performática sem a sobrecarga tradicional do RxJS.
O que são Angular Signals?
Pense em signals como "variáveis inteligentes" que automaticamente rastreiam quando são lidas e notificam quando mudam. São como um rastreador GPS para seus dados - sempre sabendo quem está observando e atualizando eficientemente apenas o que precisa mudar.
Angular Signals resolvem o problema fundamental da reatividade granular: saber exatamente o que mudou e atualizar apenas as partes afetadas da sua UI, sem gerenciamento manual de subscrições ou preocupações com change detection.
Quando Você Deve Usar Angular Signals?
Bons casos de uso:
Gerenciamento de estado de componentes que precisa de atualizações reativas
Valores computados derivados de outras fontes reativas
Estado e lógica de validação de formulários
Estado compartilhado entre componentes sem services
Atualizações de UI críticas para performance com change detection mínimo
Quando NÃO usar Signals:
Requisições HTTP e operações assíncronas (continue com Observables)
Streams de eventos complexos que precisam de operators como debounce, throttle
Integração com codebases pesadas em RxJS (use interop com cuidado)
Signals: Sua Primeira Implementação
Vamos construir um exemplo prático: um contador de produtos com cálculo de preço em tempo real que demonstra os conceitos principais de signals.
Passo 1: Criando Seu Primeiro Signal
import { Component, signal } from '@angular/core';@Component({ selector: 'app-produto', template: ` <div> <h2>Produto: {{ nomeProduto() }}</h2> <p>Quantidade: {{ quantidade() }}</p> <button (click)="incrementar()">Adicionar ao Carrinho</button> </div> `})export class ProdutoComponent { // Criando signals graváveis nomeProduto = signal('Livro Angular'); quantidade = signal(0); incrementar() { // Atualizando valor do signal this.quantidade.set(this.quantidade() + 1); }}Este código cria dois signals - note como os chamamos como funções no template. Signals são funções que retornam seu valor atual quando chamadas.
Passo 2: Trabalhando com Computed Signals
import { Component, signal, computed } from '@angular/core';@Component({ selector: 'app-produto', template: ` <div> <p>Quantidade: {{ quantidade() }}</p> <p>Preço unitário: R$ {{ precoUnitario() }}</p> <p>Total: R$ {{ precoTotal() }}</p> <button (click)="incrementar()">Adicionar Item</button> </div> `})export class ProdutoComponent { quantidade = signal(1); precoUnitario = signal(29.99); // Computed signal atualiza automaticamente quando dependências mudam precoTotal = computed(() => { return this.quantidade() * this.precoUnitario(); }); incrementar() { this.quantidade.update(q => q + 1); }}Computed signals recalculam automaticamente quando suas dependências mudam. Sem subscrições, sem atualizações manuais - simplesmente funciona.
Passo 3: Signal Effects para Side Effects
import { Component, signal, computed, effect } from '@angular/core';@Component({ selector: 'app-produto'})export class ProdutoComponent { quantidade = signal(0); estoque = signal(10); constructor() { // Effect executa sempre que signals lidos mudam effect(() => { if (this.quantidade() > this.estoque()) { console.log('Aviso: Quantidade excede o estoque!'); this.mostrarAvisoEstoque = true; } }); } adicionarAoCarrinho() { if (this.quantidade() < this.estoque()) { this.quantidade.update(q => q + 1); } }}Effects rastreiam automaticamente dependências de signals e re-executam quando esses signals mudam - perfeito para logging, analytics, ou manipulações do DOM.
Um Exemplo Mais Complexo: Carrinho de Compras com Filtros
Vamos construir algo mais realista - um carrinho de compras com filtragem e cálculos em tempo real:
import { Component, signal, computed } from '@angular/core';interface Produto { id: number; nome: string; preco: number; categoria: string; emEstoque: boolean;}@Component({ selector: 'app-carrinho-compras', template: ` <div class="carrinho"> <input placeholder="Buscar produtos..." (input)="termoBusca.set($event.target.value)" /> <select (change)="categoriaSelecionada.set($event.target.value)"> <option value="todas">Todas Categorias</option> <option *ngFor="let cat of categorias()" [value]="cat"> {{ cat }} </option> </select> <div class="produtos"> <div *ngFor="let produto of produtosFiltrados()"> <h3>{{ produto.nome }}</h3> <p>R$ {{ produto.preco }}</p> <button (click)="adicionarAoCarrinho(produto)" [disabled]="!produto.emEstoque" > Adicionar ao Carrinho </button> </div> </div> <div class="resumo"> <p>Itens no carrinho: {{ itensCarrinho().length }}</p> <p>Total: R$ {{ totalCarrinho() }}</p> <p>Com imposto (10%): R$ {{ totalComImposto() }}</p> </div> </div> `})export class CarrinhoComprasComponent { // Signals de estado produtos = signal<Produto[]>([ { id: 1, nome: 'Notebook', preco: 3999, categoria: 'Eletrônicos', emEstoque: true }, { id: 2, nome: 'Mouse', preco: 89, categoria: 'Eletrônicos', emEstoque: true }, { id: 3, nome: 'Mesa', preco: 899, categoria: 'Móveis', emEstoque: false }, { id: 4, nome: 'Cadeira', preco: 599, categoria: 'Móveis', emEstoque: true } ]); itensCarrinho = signal<Produto[]>([]); termoBusca = signal(''); categoriaSelecionada = signal('todas'); // Computed signals para estado derivado categorias = computed(() => { const cats = new Set(this.produtos().map(p => p.categoria)); return Array.from(cats); }); produtosFiltrados = computed(() => { const termo = this.termoBusca().toLowerCase(); const categoria = this.categoriaSelecionada(); return this.produtos().filter(produto => { const correspondeBusca = produto.nome.toLowerCase().includes(termo); const correspondeCategoria = categoria === 'todas' || produto.categoria === categoria; return correspondeBusca && correspondeCategoria; }); }); totalCarrinho = computed(() => { return this.itensCarrinho().reduce((soma, item) => soma + item.preco, 0); }); totalComImposto = computed(() => { return this.totalCarrinho() * 1.1; // 10% imposto }); adicionarAoCarrinho(produto: Produto) { this.itensCarrinho.update(items => [...items, produto]); }}Este exemplo mostra como signals lidam elegantemente com relacionamentos reativos complexos sem gerenciamento manual de subscrições.
Padrão Avançado: Validação de Formulário Baseada em Signals
Vamos construir algo ainda mais sofisticado - um formulário reativo com validação em tempo real usando signals:
import { Component, signal, computed, effect } from '@angular/core';interface ErrosFormulario { email?: string; senha?: string; confirmarSenha?: string;}@Component({ selector: 'app-formulario-cadastro', template: ` <form (submit)="enviarFormulario($event)"> <div> <input type="email" placeholder="Email" [value]="email()" (input)="email.set($event.target.value)" [class.erro]="erros().email" /> <span class="msg-erro">{{ erros().email }}</span> </div> <div> <input type="password" placeholder="Senha" [value]="senha()" (input)="senha.set($event.target.value)" [class.erro]="erros().senha" /> <span class="msg-erro">{{ erros().senha }}</span> </div> <div> <input type="password" placeholder="Confirmar Senha" [value]="confirmarSenha()" (input)="confirmarSenha.set($event.target.value)" [class.erro]="erros().confirmarSenha" /> <span class="msg-erro">{{ erros().confirmarSenha }}</span> </div> <button [disabled]="!formularioValido()"> Cadastrar </button> <div class="medidor-forca"> Força da Senha: {{ forcaSenha() }} </div> </form> `})export class FormularioCadastroComponent { // Signals dos campos do formulário email = signal(''); senha = signal(''); confirmarSenha = signal(''); camposTocados = signal<Set<string>>(new Set()); // Regras de validação como computed signals erros = computed<ErrosFormulario>(() => { const erros: ErrosFormulario = {}; const tocados = this.camposTocados(); // Validação de email if (tocados.has('email')) { const valorEmail = this.email(); if (!valorEmail) { erros.email = 'Email é obrigatório'; } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(valorEmail)) { erros.email = 'Formato de email inválido'; } } // Validação de senha if (tocados.has('senha')) { const pwd = this.senha(); if (!pwd) { erros.senha = 'Senha é obrigatória'; } else if (pwd.length < 8) { erros.senha = 'Senha deve ter pelo menos 8 caracteres'; } } // Validação de confirmação de senha if (tocados.has('confirmarSenha')) { if (this.senha() !== this.confirmarSenha()) { erros.confirmarSenha = 'Senhas não coincidem'; } } return erros; }); forcaSenha = computed(() => { const pwd = this.senha(); if (pwd.length < 6) return 'Fraca'; if (pwd.length < 10) return 'Média'; if (/[A-Z]/.test(pwd) && /[0-9]/.test(pwd) && /[^A-Za-z0-9]/.test(pwd)) { return 'Forte'; } return 'Média'; }); formularioValido = computed(() => { return this.email() && this.senha() && this.confirmarSenha() && Object.keys(this.erros()).length === 0; }); constructor() { // Auto-salvar rascunho no localStorage effect(() => { const rascunho = { email: this.email(), timestamp: Date.now() }; localStorage.setItem('rascunhoCadastro', JSON.stringify(rascunho)); }); } marcarComoTocado(campo: string) { this.camposTocados.update(campos => { campos.add(campo); return new Set(campos); }); } enviarFormulario(event: Event) { event.preventDefault(); if (this.formularioValido()) { console.log('Formulário enviado:', { email: this.email(), senha: this.senha() }); } }}Isso demonstra como signals podem substituir bibliotecas de formulários complexas com lógica de validação simples e reativa.
Angular Signals com TypeScript
Para usuários TypeScript, veja como tornar suas implementações de signal type-safe:
// tipos.tsinterface Usuario { id: number; nome: string; email: string; papel: 'admin' | 'usuario' | 'convidado';}interface EstadoApp { usuarioAtual: Usuario | null; estaAutenticado: boolean; permissoes: string[];}// signal-store.service.tsimport { Injectable, signal, computed, Signal } from '@angular/core';@Injectable({ providedIn: 'root' })export class SignalStore { // Signals graváveis com tipos explícitos private _usuarioAtual = signal<Usuario | null>(null); private _estaCarregando = signal<boolean>(false); // Computed signals somente leitura readonly usuarioAtual: Signal<Usuario | null> = this._usuarioAtual.asReadonly(); readonly estaAutenticado = computed<boolean>(() => !!this._usuarioAtual()); readonly permissoes = computed<string[]>(() => { const usuario = this._usuarioAtual(); if (!usuario) return []; switch(usuario.papel) { case 'admin': return ['ler', 'escrever', 'deletar', 'admin']; case 'usuario': return ['ler', 'escrever']; case 'convidado': return ['ler']; } }); // Métodos de atualização type-safe login(usuario: Usuario): void { this._usuarioAtual.set(usuario); } atualizarUsuario(atualizacoes: Partial<Usuario>): void { this._usuarioAtual.update(atual => atual ? { ...atual, ...atualizacoes } : null ); }}// Uso com TypeScript@Component({ selector: 'app-perfil', template: ` <div *ngIf="store.usuarioAtual() as usuario"> <h2>{{ usuario.nome }}</h2> <p>Papel: {{ usuario.papel }}</p> <ul> <li *ngFor="let perm of store.permissoes()"> {{ perm }} </li> </ul> </div> `})export class PerfilComponent { constructor(public store: SignalStore) {}}Padrões Avançados e Melhores Práticas
1. Padrão de Composição de Signals
Crie signals de ordem superior que combinam múltiplas fontes de signals:
// Compor múltiplos signals em um único estado reativofunction criarListaPaginada<T>(itens: Signal<T[]>, tamanhoPagina: number) { const paginaAtual = signal(0); const totalPaginas = computed(() => Math.ceil(itens().length / tamanhoPagina) ); const itensPaginados = computed(() => { const inicio = paginaAtual() * tamanhoPagina; return itens().slice(inicio, inicio + tamanhoPagina); }); return { itens: itensPaginados, paginaAtual: paginaAtual.asReadonly(), totalPaginas, proximaPagina: () => paginaAtual.update(p => Math.min(p + 1, totalPaginas() - 1)), paginaAnterior: () => paginaAtual.update(p => Math.max(p - 1, 0)) };}2. Padrão de Memoização de Signals
Otimize computações caras com signals memoizados:
// Memoizar operações carasfunction criarSignalMemoizado<T, R>( fonte: Signal<T>, computar: (valor: T) => R, igualdade?: (a: R, b: R) => boolean) { let ultimaEntrada: T | undefined; let ultimaSaida: R | undefined; return computed(() => { const atual = fonte(); if (ultimaEntrada === atual && ultimaSaida !== undefined) { return ultimaSaida; } ultimaEntrada = atual; ultimaSaida = computar(atual); return ultimaSaida; }, { equal: igualdade });}3. Padrão de Debouncing de Signals
Implemente signals com debounce para busca e manipulação de inputs:
// Signal com debounce para inputs de buscafunction criarSignalComDebounce<T>(valorInicial: T, atraso: number) { const imediato = signal(valorInicial); const comDebounce = signal(valorInicial); let timeoutId: any; const definir = (valor: T) => { imediato.set(valor); clearTimeout(timeoutId); timeoutId = setTimeout(() => { comDebounce.set(valor); }, atraso); }; return { imediato: imediato.asReadonly(), comDebounce: comDebounce.asReadonly(), definir };}// Usoconst busca = criarSignalComDebounce('', 300);// busca.imediato() - valor instantâneo// busca.comDebounce() - valor com debounce para chamadas de API4. Padrão de State Machine com Signals
Construa state machines robustas com signals:
// State machine usando signalsfunction criarMaquinaDeEstados<T extends string>( estadoInicial: T, transicoes: Record<T, T[]>) { const estadoAtual = signal(estadoInicial); const podeTransicionarPara = computed(() => { return transicoes[estadoAtual()] || []; }); const transicionarPara = (novoEstado: T) => { if (podeTransicionarPara().includes(novoEstado)) { estadoAtual.set(novoEstado); return true; } return false; }; return { estado: estadoAtual.asReadonly(), podeTransicionarPara, transicionarPara };}Armadilhas Comuns a Evitar
1. Mutar Objetos Dentro de Signals
// ❌ Não faça isso - mutar objeto não dispara atualizaçõesconst usuario = signal({ nome: 'João', idade: 30 });usuario().nome = 'Maria'; // Isso não disparará change detection!// ✅ Faça isso - crie nova referência de objetousuario.update(u => ({ ...u, nome: 'Maria' }));// Ouusuario.set({ ...usuario(), nome: 'Maria' });2. Criar Signals Dentro de Computed
// ❌ Exemplo problemático - cria novo signal em cada computaçãoconst computedRuim = computed(() => { const signalTemp = signal(0); // Não crie signals aqui! return signalTemp() + outroSignal();});// ✅ Solução - crie signals fora do computedconst signalTemp = signal(0);const computedBom = computed(() => { return signalTemp() + outroSignal();});3. Esquecer de Chamar Funções Signal
// ❌ Evite esse padrão - esquecendo parênteses@Component({ template: `<div>{{ contador }}</div>` // Não atualizará!})export class ComponenteRuim { contador = signal(0);}// ✅ Abordagem preferida - sempre chame signals como funções@Component({ template: `<div>{{ contador() }}</div>` // Propriamente reativo})export class ComponenteBom { contador = signal(0);}Quando NÃO Usar Signals
Não use signals quando:
Trabalhando com requisições HTTP - Observables lidam melhor com operações assíncronas
Precisar de operators de stream complexos (debounce, throttle, retry) - RxJS é mais poderoso
Integrando com APIs baseadas em Observable existentes - conversão desnecessária
// ❌ Exagero para cenários simplesconst resultadoHttp = signal<Dados | null>(null);this.http.get('/api/dados').subscribe(dados => { resultadoHttp.set(dados); // Conversão desnecessária});// ✅ Solução simples é melhordados$ = this.http.get('/api/dados');// Use async pipe no templateSignals vs RxJS Observables
Signals são ótimos para:
Gerenciamento de estado síncrono
Valores computados simples
Atualizações de UI críticas para performance
Reduzir código boilerplate
Considere Observables quando precisar:
Operações assíncronas → Requisições HTTP, streams WebSocket
Operators complexos → debounceTime, switchMap, retry
Streams de eventos → fromEvent, interval, timer
Conclusão
Angular Signals são uma ferramenta poderosa que pode simplificar drasticamente o gerenciamento de estado em suas aplicações. Eles trazem reatividade granular, rastreamento automático de dependências, e melhor performance para seus componentes Angular.
Principais aprendizados:
Signals são funções que mantêm e rastreiam valores reativos
Computed signals derivam automaticamente estado de outros signals
Effects lidam com side effects com rastreamento automático de dependências
Signals eliminam gerenciamento manual de subscrições e memory leaks
Da próxima vez que você for usar um Subject ou BehaviorSubject para estado de componente, lembre-se dos signals. Seu código será mais limpo, mais performático, e mais fácil de entender.
Já começou a usar signals em seus projetos Angular? Que padrões você descobriu? Compartilhe suas experiências nos comentários!


