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 API4. 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 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!
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é!
100% do apoio vai para mais artigos técnicos gratuitos para a comunidade dev brasileira! 🚀
