Fala, devs! 👋

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.ts
interface Usuario {
  id: number;
  nome: string;
  email: string;
  papel: 'admin' | 'usuario' | 'convidado';
}

interface EstadoApp {
  usuarioAtual: Usuario | null;
  estaAutenticado: boolean;
  permissoes: string[];
}

// signal-store.service.ts
import { 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 reativo
function 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 caras
function 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 busca
function 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
  };
}

// Uso
const 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:

// State machine usando signals
function 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ções
const usuario = signal({ nome: 'João', idade: 30 });
usuario().nome = 'Maria'; // Isso não disparará change detection!

// ✅ Faça isso - crie nova referência de objeto
usuario.update(u => ({ ...u, nome: 'Maria' }));
// Ou
usuario.set({ ...usuario(), nome: 'Maria' });

2. Criar Signals Dentro de Computed

// ❌ Exemplo problemático - cria novo signal em cada computação
const computedRuim = computed(() => {
  const signalTemp = signal(0); // Não crie signals aqui!
  return signalTemp() + outroSignal();
});

// ✅ Solução - crie signals fora do computed
const 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 simples
const resultadoHttp = signal<Dados | null>(null);
this.http.get('/api/dados').subscribe(dados => {
  resultadoHttp.set(dados); // Conversão desnecessária
});

// ✅ Solução simples é melhor
dados$ = 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!


Se isso ajudou você a melhorar suas habilidades em Angular, siga para mais padrões e melhores práticas modernas de Angular! 🚀

Recursos


☕ Apoie Conteúdo Técnico Gratuito

Se este artigo te ajudou a entender Angular Signals ou economizou tempo de debug, considere me apoiar com um café!

☕ Me pague um café

100% do apoio vai para mais artigos técnicos gratuitos para a comunidade dev brasileira! 🚀