Capítulo 06: Reatividade com Angular Signals (signal, computed, effect)
Capítulo 06: Reatividade com Angular Signals (signal, computed, effect)
Especialização em Front-End com Angular • Angular 18+ • Signals, Standalone Architecture, RxJS, NgRx e SSR
🗺️ Mapa Conceitual do Grafo Reativo dos Signals
flowchart TD
subgraph WRITABLE["Produtores Primários (Writable Signals)"]
S1["preco = signal(100)"]
S2["quantidade = signal(2)"]
S3["desconto = signal(0.10)"]
end
subgraph DERIVED["Consumidores & Produtores Derivados (Computed Signals)"]
C1["subtotal = computed(() => preco() * quantidade())"]
C2["total = computed(() => subtotal() * (1 - desconto()))"]
end
subgraph EFFECTS["Consumidores Finais (Efeitos Colaterais & UI)"]
E1["effect(() => console.log('Total:', total()))"]
E2["Template View: {{ total() | currency }}"]
end
S1 & S2 --> C1
C1 & S3 --> C2
C2 --> E1 & E2
style WRITABLE fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style DERIVED fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style EFFECTS fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
🏛️ 1. O Modelo Mental dos Signals no Angular
Os Signals são a espinha dorsal da nova arquitetura reativa do Angular. Um Signal é um invólucro reativo em torno de um valor que notifica consumidores interessados sempre que esse valor se altera.
Ao contrário de sistemas reativos baseados em sujas varreduras completas do DOM (dirty checking via Zone.js), os Signals utilizam um modelo Push-Pull Glitch-Free:
- Push: Notifica o grafo de que uma dependência mudou de estado.
- Pull: Computa o novo valor sob demanda (lazy evaluation) apenas no momento exato em que a leitura for solicitada por um template ou efeito.
- Glitch-Free: Garante que estados intermediários inconsistentes nunca sejam observados pela aplicação.
⚙️ 2. Primitivas Fundamentais de Signals
2.1 Writable Signal: Leitura e Escrita
Criado via função signal(valorInicial). Retorna um WritableSignal<T> com métodos para atualização:
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-contador',
standalone: true,
template: `
<div class="painel">
<p>Valor Atual: <strong>{{ contador() }}</strong></p>
<button (click)="incrementar()">+1</button>
<button (click)="decrementar()">-1</button>
<button (click)="resetar()">Zerar</button>
</div>
`
})
export class ContadorComponent {
contador = signal<number>(0);
incrementar(): void {
// .update() recebe o valor anterior e retorna o novo
this.contador.update(valorAnterior => valorAnterior + 1);
}
decrementar(): void {
this.contador.update(v => Math.max(0, v - 1));
}
resetar(): void {
// .set() substitui o valor incondicionalmente
this.contador.set(0);
}
}
2.2 Computed Signal: Valores Derivados e Memoizados
Criado via função computed(() => expressao). Um computed é somente-leitura e rastreia dinamicamente quais signals são lidos dentro da sua função executora. Se nenhum dos signals dependentes mudar, leituras subsequentes retornam o valor em cache instantaneamente em $O(1)$.
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-carrinho-compras',
standalone: true,
template: `
<div>
<p>Preço: {{ preco() }} | Quantidade: {{ quantidade() }}</p>
<p>Subtotal: {{ subtotal() }}</p>
<p>Total com Frete: {{ totalComFrete() }}</p>
</div>
`
})
export class CarrinhoComponent {
preco = signal(150);
quantidade = signal(3);
freteGratis = signal(false);
// Rastreia dinamicamente preco() e quantidade()
subtotal = computed(() => this.preco() * this.quantidade());
// Rastreamento condicional dinâmico
totalComFrete = computed(() => {
const base = this.subtotal();
if (this.freteGratis() || base > 300) {
return base; // Não lê o valor do frete se for grátis
}
return base + 25;
});
}
2.3 Effect: Efeitos Colaterais Controlados
Criado via effect(() => ...). É executado de forma assíncrona após as alterações de estado, ideal para sincronizar com APIs de terceiros, localStorage, WebSockets ou manipulações imperativas do DOM:
import { Component, signal, effect, inject } from '@angular/core';
@Component({
selector: 'app-tema-usuario',
standalone: true,
template: `<button (click)="alternarTema()">Mudar para {{ proximoTema() }}</button>`
})
export class TemaUsuarioComponent {
tema = signal<'claro' | 'escuro'>('claro');
proximoTema = computed(() => this.tema() === 'claro' ? 'escuro' : 'claro');
constructor() {
// O effect precisa ser registrado em um contexto de injeção (construtor)
effect((onCleanup) => {
const temaAtual = this.tema();
document.body.setAttribute('data-theme', temaAtual);
localStorage.setItem('user-theme', temaAtual);
// Função de limpeza opcional (executa antes da próxima execução ou na destruição)
onCleanup(() => {
console.log(`Limpando configurações do tema anterior: ${temaAtual}`);
});
});
}
alternarTema(): void {
this.tema.update(t => t === 'claro' ? 'escuro' : 'claro');
}
}
🔍 3. Técnicas Avançadas com Signals
3.1 Isolando Dependências com untracked()
Quando você precisa ler um signal dentro de um computed ou effect sem registrar aquele signal como dependência reativa:
import { effect, untracked } from '@angular/core';
effect(() => {
const usuario = this.usuarioLogado(); // Rastreia: reexecuta se o usuário mudar
// Lê as configurações sem reexecutar o effect caso elas mudem
const configs = untracked(() => this.configuracoesLocais());
analyticsService.logPageView(usuario.id, configs.debugMode);
});
3.2 Custom Equality Functions
Por padrão, os signals utilizam Object.is para saber se o valor mudou. Para objetos ou coleções com comparação estrutural customizada:
const usuario = signal({ id: 1, nome: 'Ana' }, {
equal: (a, b) => a.id === b.id && a.nome === b.nome
});
⚖️ 4. Comparativo: Signals vs Observables (RxJS) vs Zone.js
| Critério | Angular Signals | RxJS Observables | Zone.js Tradicional |
|---|---|---|---|
| Natureza | Estado síncrono e valor pontual | Streams de eventos assíncronos | Monkey-patching de APIs do browser |
| Sincronismo | Síncrono (signal()) |
Assíncrono ou Síncrono | Assíncrono |
| Necessidade de Cleanup | Não precisa de unsubscribe | Obrigatório (takeUntilDestroyed) |
Automático via Monkey-patching |
| Interoperabilidade | toSignal(), toObservable() |
Fonte primária assíncrona | Acoplado ao framework legada |
| Performance de Render | Granularidade Fina (Zoneless) | Depende do AsyncPipe |
Varredura de árvore inteira |
⚠️ 5. Armadilhas Críticas ao Usar Signals
- Escrever em Signals dentro de um
computed:- Regra inviolável: A função dentro de
computed()DEVE SER PURA e livre de efeitos colaterais. NUNCA façameuOutroSignal.set()dentro de um computed. O Angular disparará um erro de runtime.
- Regra inviolável: A função dentro de
- Ciclos Infinitos em
effect():- Se um
effectaltera um signal que ele mesmo consome, ele entrará em loop infinito. Por padrão, o Angular desabilitaallowSignalWrites: trueem effects exatamente para forçar um fluxo de dados unidirecional sadio.
- Se um
- Instanciar
effect()Fora de um Injection Context:- Chamar
effect()em métodos comuns ou fora doconstructorgera o erroNG0203: effect() must be called in an injection context. Se necessário criar fora do construtor, passe explicitamente{ injector: this.injector }.
- Chamar
🛡️ 6. Boas Práticas e Padrões Arquiteturais
- Imutabilidade em Coleções: Embora o método
.mutate()tenha sido descontinuado, utilize sempre atualizações imutáveis com.update():// Adicionar item this.itens.update(lista => [...lista, novoItem]); // Remover item this.itens.update(lista => lista.filter(i => i.id !== idRemover)); - Evitar Abuso de Effects: Use
computed()para 90% dos casos de sincronização de dados. Reserveeffect()estritamente para logging, persistência local, chamadas de bibliotecas de terceiros imperativas (D3, Chart.js, Leaflet) ou mutações do DOM real.
📋 7. Cheat-Sheet: Resumo Executivo
const count = signal(0); // WritableSignal
count.set(10); // Substitui
count.update(c => c + 1); // Deriva do anterior
const double = computed(() => count() * 2);// Computed (Readonly)
effect(() => { // Effect (Side-effect)
console.log('Valor atual:', count());
});