Guias & Tutoriais

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…

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 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

TYPESCRIPT
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

TYPESCRIPT
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

TYPESCRIPT
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:

TYPESCRIPT
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:

TYPESCRIPT
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:

JAVASCRIPT
// 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:

JAVASCRIPT
// 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:

JAVASCRIPT
// 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:

JAVASCRIPT
// 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 API

4. Padrão de State Machine com Signals

Construa state machines robustas com signals:

JAVASCRIPT
// 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

JAVASCRIPT
// ❌ 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

JAVASCRIPT
// ❌ 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

TSX
// ❌ 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

JAVASCRIPT
// ❌ 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 template

Signals 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!


Recursos