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.
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.
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)
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.
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.
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.
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.
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.
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.
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) {}}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)) };}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 });}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 APIConstrua 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 };}// ❌ 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' });// ❌ 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();});// ❌ 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);}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 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
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!