Identificação
- Duração: 2 horas
- Tipo: teoria aplicada e laboratório
- Entrega: store reativo de tarefas com estado fonte e derivado
- Laboratório:
../exemplos/aula-4.8/index.html
Introdução
O que é estado?
Estado é qualquer informação que pode mudar enquanto a aplicação está aberta e cuja mudança pode alterar o que o usuário vê ou pode fazer.
usuário marca uma tarefa
↓
o estado da tarefa muda
↓
lista, contador e detalhes se atualizam
O título de uma tarefa, o filtro selecionado e a resposta de uma API podem ser estado. Uma constante como o nome do produto, que nunca muda durante a execução, não precisa ser tratada como estado reativo.
Estado é a mesma coisa que banco de dados?
Não. O banco mantém dados no servidor. O estado da interface vive no navegador e serve para renderizar a experiência atual. Uma tarefa pode existir no MySQL e uma cópia dela chegar ao Angular por HTTP.
MySQL → Prisma → NestJS → HTTP → estado Angular → template
O Angular não acessa MySQL diretamente. Ele mantém apenas os dados necessários para a tela e envia alterações para a API conforme o contrato.
Estado é qualquer variável?
Não. Uma variável local usada por alguns milissegundos para calcular um resultado não precisa notificar a interface. Estado reativo existe quando consumidores precisam saber que um valor mudou.
const tax = 0.1; // constante comum
const count = signal(0); // estado reativo
const total = computed(() => count() * 10); // valor derivado
O que é reatividade?
Reatividade é a capacidade de propagar mudanças automaticamente para quem depende de um valor. Em vez de procurar manualmente todos os lugares que exibem uma tarefa, nós declaramos as dependências e o Angular atualiza os consumidores afetados.
O que é um signal?
Signal é um contêiner reativo do Angular. Ele guarda um valor atual e informa aos consumidores quando esse valor muda. Para ler, chamamos o signal como função.
const count = signal(0);
console.log(count()); // leitura: 0
count.set(1); // substituição: 1
count.update((value) => value + 1); // atualização: 2
Os parênteses de count() não executam uma requisição. Eles permitem ao Angular
rastrear a leitura e formar um grafo de dependências.
Signal é igual a Observable?
Não. Signal sempre possui um valor atual e é lido de maneira síncrona. Observable
representa um fluxo que pode emitir valores ao longo do tempo. Eles podem trabalhar
juntos com toSignal e toObservable, mas um não substitui automaticamente todos os
usos do outro.
| Signal | Observable |
|---|---|
| valor atual disponível | sequência no tempo |
leitura com value() |
consumo por inscrição |
| ideal para estado da interface | ideal para eventos e operações assíncronas |
| dependências rastreadas | composição por operadores RxJS |
O que é computed?
computed cria um signal somente leitura calculado a partir de outros signals. Ele
é ideal para totais, filtros, permissões e textos que podem ser deduzidos.
const tasks = signal<readonly Task[]>([]);
const openCount = computed(
() => tasks().filter((task) => !task.done).length,
);
Não guardamos openCount separadamente, porque isso criaria duas fontes de verdade.
O que é linkedSignal?
linkedSignal é um estado gravável que depende de outra fonte. Imagine uma tarefa
selecionada: o usuário pode trocar a seleção, mas, se a lista mudar e remover essa
tarefa, a seleção precisa voltar para uma opção válida.
Ele não é um computed: ambos reagem a dependências, porém computed é somente
leitura e linkedSignal continua aceitando set.
O que é effect?
effect executa uma ação quando signals lidos por ele mudam. Use-o para conversar
com APIs não reativas, como telemetria, armazenamento local ou uma biblioteca de
gráfico.
effect(() => {
document.title = `${openCount()} tarefas abertas`;
});
effect não deve ser a ferramenta padrão para copiar um signal para outro. Se um
valor pode ser calculado, use computed; se é um estado gravável dependente, avalie
linkedSignal.
Como isso entra na Knowledge AI?
O painel terá uma lista de tarefas como estado fonte. Filtro, contagem e tarefa selecionada serão conectados por um grafo reativo:
tasks ────────┬──▶ visibleTasks ──▶ template da lista
├──▶ openCount ─────▶ indicador
└──▶ selectedId ────▶ selectedTask ──▶ detalhes
filter ───────────▶ visibleTasks
Objetivos
Ao final da aula, o aluno deverá conseguir:
- explicar estado, reatividade e fonte de verdade;
- distinguir estado de domínio, remoto, local, visual, de formulário e derivado;
- criar e ler writable signals;
- atualizar valores com
seteupdate; - expor signals somente leitura com
asReadonly; - criar valores derivados com
computed; - explicar avaliação lazy, cache e dependências dinâmicas;
- preservar estado dependente válido com
linkedSignal; - usar
effectapenas para efeitos colaterais; - reconhecer quando usar
untrackede igualdade personalizada; - atualizar arrays e objetos sem mutação invisível;
- integrar signals e RxJS sem duplicar subscriptions.
Pré-requisitos
- Aulas 4.1 a 4.7 concluídas;
- componentes, serviços e injeção de dependência;
- arrays e objetos imutáveis;
- templates e control flow;
- noções de Observable e
toSignal.
Pergunta orientadora
Como fazer uma mudança atualizar exatamente os consumidores necessários sem manter cópias inconsistentes do mesmo dado?
Roteiro sugerido
| Etapa | Duração |
|---|---|
| Estado e fonte de verdade | 20 min |
signal e computed |
30 min |
linkedSignal e effect |
25 min |
| Store e imutabilidade | 25 min |
| Laboratório | 15 min |
| Revisão | 5 min |
1. Classificando o estado
| Categoria | Exemplo | Local comum |
|---|---|---|
| domínio | tarefas recebidas da API | serviço/store |
| remoto/assíncrono | loading, erro e resultado HTTP | resource ou fluxo RxJS |
| visual/local | painel aberto, aba ativa | componente |
| formulário | valores, erros, dirty e pending | formulário |
| navegação | rota e parâmetros | Angular Router |
| derivado | tarefas visíveis, total em aberto | computed |
A classificação orienta propriedade e ciclo de vida. Não transforme todo dado em estado global.
2. Fonte de verdade
Uma informação deve possuir um lugar autoritativo. Se tasks já informa quais itens
estão abertos, armazenar também openCount exige sincronização manual.
// Evite duas fontes graváveis.
const tasks = signal<readonly Task[]>([]);
const openCount = signal(0);
// Prefira derivação.
const openCount = computed(
() => tasks().filter((task) => !task.done).length,
);
3. Criando um writable signal
private readonly taskState = signal<readonly Task[]>(INITIAL_TASKS);
O tipo WritableSignal<readonly Task[]> permite escrever no contêiner, enquanto
readonly Task[] impede alterações acidentais na coleção recebida.
4. Leitura rastreada
const currentTasks = this.taskState();
Quando a leitura acontece em um computed, effect ou template, o Angular registra
a dependência. Ao mudar o signal, ele invalida os consumidores relacionados.
5. set e update
this.filterState.set("open");
this.taskState.update((tasks) =>
tasks.map((task) =>
task.id === id ? { ...task, done: !task.done } : task,
),
);
Use set quando já possui o próximo valor e update quando ele depende do anterior.
6. Não mutar silenciosamente
// Evite: a referência do array continua igual.
this.taskState().push(newTask);
// Prefira: produz uma nova referência.
this.taskState.update((tasks) => [...tasks, newTask]);
Por padrão, signals comparam valores com Object.is. Uma mutação interna não passa
por set ou update e pode deixar consumidores desatualizados.
7. Estado privado, leitura pública
private readonly taskState = signal<readonly Task[]>(INITIAL_TASKS);
readonly tasks = this.taskState.asReadonly();
O componente pode ler tasks(), mas só os métodos do store alteram a coleção. Isso
protege invariantes sem esconder o valor atual.
8. Derivando com computed
readonly visibleTasks = computed(() => {
const filter = this.filter();
return this.tasks().filter((task) => {
if (filter === "open") return !task.done;
if (filter === "done") return task.done;
return true;
});
});
visibleTasks depende de filter e tasks. O template apenas lê o resultado.
9. Lazy e memoizado
A função de um computed só calcula quando o valor é lido. O resultado fica em cache
até uma dependência mudar. Leituras repetidas não refazem o filtro sem necessidade.
10. Dependências dinâmicas
Somente os signals realmente lidos durante a execução tornam-se dependências.
readonly summary = computed(() => {
if (!this.showSummary()) return "Resumo oculto";
return `${this.openCount()} tarefas abertas`;
});
Quando showSummary() é falso, openCount não é lido e deixa de ser uma dependência
daquela execução.
11. Estado dependente com linkedSignal
readonly selectedId = linkedSignal<readonly Task[], number | null>({
source: this.tasks,
computation: (tasks, previous) => {
const previousId = previous?.value ?? null;
return tasks.some((task) => task.id === previousId)
? previousId
: tasks[0]?.id ?? null;
},
});
O usuário ainda pode executar selectedId.set(3). Quando tasks muda, a computação
preserva o ID anterior se ele continua válido; caso contrário escolhe o primeiro.
12. Seleção derivada
readonly selectedTask = computed(() => {
const id = this.selectedId();
return this.tasks().find((task) => task.id === id) ?? null;
});
Guardamos apenas o ID gravável e derivamos o objeto atual. Assim, uma atualização na tarefa aparece nos detalhes sem copiar manualmente o objeto selecionado.
13. computed ou linkedSignal?
| Pergunta | Escolha |
|---|---|
| o usuário nunca escreve esse valor? | computed |
| o usuário escreve, mas outra fonte pode reinicializá-lo? | linkedSignal |
| o valor é independente? | signal |
| preciso chamar uma API não reativa? | effect ou integração específica |
14. Efeito colateral
Um efeito colateral altera algo fora do cálculo puro: título do documento,
telemetria, localStorage, canvas ou biblioteca externa.
private readonly titleEffect = effect(() => {
this.document.title = `${this.openCount()} abertas | Knowledge AI`;
});
No Angular, effect exige um contexto de injeção, salvo quando um Injector é
fornecido. Efeitos criados nesse contexto são destruídos junto com ele.
15. Não propagar estado com effect
// Evite.
effect(() => this.openCountState.set(countOpen(this.tasks())));
// Prefira.
readonly openCount = computed(() => countOpen(this.tasks()));
A primeira opção cria escrita indireta, ordem de execução difícil e risco de ciclos.
16. Leitura incidental com untracked
effect(() => {
const count = this.openCount();
const user = untracked(this.currentUser);
this.analytics.record(user.id, count);
});
O efeito depende de openCount, não de currentUser. Use untracked somente quando
a leitura é realmente incidental; esconder uma dependência necessária cria bugs.
17. Igualdade personalizada
const filters = signal(
{ status: "all" as TaskFilter },
{ equal: (a, b) => a.status === b.status },
);
A função decide se o novo valor é relevante para consumidores. Comparação profunda em estruturas grandes pode custar mais do que uma renderização; use com evidência.
18. Store reativo completo
import { Service, computed, linkedSignal, signal } from "@angular/core";
@Service()
export class TaskStore {
private readonly taskState = signal<readonly Task[]>(INITIAL_TASKS);
private readonly filterState = signal<TaskFilter>("all");
readonly tasks = this.taskState.asReadonly();
readonly filter = this.filterState.asReadonly();
readonly openCount = computed(
() => this.tasks().filter((task) => !task.done).length,
);
readonly visibleTasks = computed(() => filterTasks(this.tasks(), this.filter()));
readonly selectedId = linkedSignal<readonly Task[], number | null>({
source: this.tasks,
computation: (tasks, previous) => {
const id = previous?.value ?? null;
return tasks.some((task) => task.id === id) ? id : tasks[0]?.id ?? null;
},
});
readonly selectedTask = computed(() =>
this.tasks().find((task) => task.id === this.selectedId()) ?? null,
);
setFilter(filter: TaskFilter): void {
this.filterState.set(filter);
}
toggle(id: number): void {
this.taskState.update((tasks) =>
tasks.map((task) => task.id === id ? { ...task, done: !task.done } : task),
);
}
}
19. Consumindo no componente
@Component({
selector: "app-task-page",
templateUrl: "./task-page.html",
providers: [TaskStore],
})
export class TaskPage {
protected readonly store = inject(TaskStore);
}
O provider no componente cria um store por instância da página. Se o estado precisa atravessar toda a aplicação, o escopo deve ser uma decisão explícita.
20. Consumindo no template
<p>{{ store.openCount() }} tarefas abertas</p>
@for (task of store.visibleTasks(); track task.id) {
<button type="button" (click)="store.selectedId.set(task.id)">
{{ task.title }}
</button>
}
Ao ler signals em um componente OnPush, o Angular rastreia as dependências e marca
o componente para atualização quando elas mudam.
21. Onde o estado deve viver?
Use o menor proprietário que atenda aos consumidores:
um componente usa → estado no componente
uma página compartilha → store provido na página
rotas compartilham → serviço em escopo superior
servidor é autoritativo → API + cache/estado remoto
URL representa a visão → Router
Globalizar cedo aumenta acoplamento e dificulta descarte do estado.
22. Estado remoto não vira local por mágica
Uma lista obtida por HTTP tem latência, erro, expiração e concorrência. O signal que guarda o último valor não elimina essas questões. Preserve o estado assíncrono da Aula 4.7 ou utilize uma abstração apropriada de resource/cache.
23. RxJS para signal
readonly tasks = toSignal(this.taskApi.list(), {
initialValue: [] as readonly TaskDto[],
});
toSignal cria uma inscrição. Crie uma vez e reutilize; não o chame repetidamente
para o mesmo Observable.
24. Signal para RxJS
readonly query = signal("");
private readonly queryChanges = toObservable(this.query);
readonly results = toSignal(
this.queryChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((query) => this.searchApi.search(query)),
),
{ initialValue: [] },
);
RxJS coordena tempo, cancelamento e requisição; signals entregam o valor atual ao template.
25. Grafo reativo
Pense em nós e arestas, não em uma sequência de comandos globais:
ação: filter.set("open")
└── invalida visibleTasks
└── atualiza lista
ação: tasks.update(...)
├── invalida visibleTasks
├── invalida openCount
└── reavalia seleção ligada
Mudar o filtro não deveria recalcular a contagem total em aberto, pois ela não lê o filtro.
26. Estado normalizado
Em coleções maiores, guardar entidades por ID pode evitar cópias inconsistentes:
interface TaskState {
readonly ids: readonly number[];
readonly entities: Readonly<Record<number, Task>>;
}
Não normalize por hábito. Uma lista pequena e local pode continuar como array.
27. Erros comuns
Guardar valor derivável
Duplicar tasks, visibleTasks e openCount como writable signals exige sincronizar
todos eles.
Esquecer os parênteses
tasks é o signal; tasks() é o valor atual.
Mutar array ou objeto por fora
tasks().push(...) não comunica uma nova referência ao grafo.
Usar effect como encanamento
Copiar estado entre signals aumenta ciclos e problemas de ordem.
Criar estado global para tudo
Uma aba aberta em uma página não precisa sobreviver na aplicação inteira.
Confundir tipagem com validação
Um signal tipado não valida o JSON que veio da API.
Recriar toSignal
Cada chamada pode criar nova inscrição e repetir trabalho assíncrono.
28. Laboratório guiado
Abra o grafo reativo de tarefas.
Etapa 1 — Altere apenas o filtro
Observe que visibleTasks recalcula, enquanto openCount e selectedTask não
dependem do filtro.
Etapa 2 — Marque uma tarefa
Veja tasks invalidar contagem, lista e detalhes.
Etapa 3 — Troque a seleção
Somente o estado ligado e os detalhes selecionados devem mudar.
Etapa 4 — Remova a tarefa selecionada
O comportamento equivalente a linkedSignal escolhe uma seleção válida.
Etapa 5 — Adicione uma tarefa
Confirme a atualização imutável da coleção e o novo grafo afetado.
Etapa 6 — Compare as fontes
Examine TaskStore, linkedSignal, template e interoperação com RxJS.
29. Sobre o laboratório estático
O laboratório roda sem compilação Angular para facilitar a exploração no navegador. Ele
simula as mesmas dependências de forma visível. A pasta src/app contém a
implementação Angular equivalente.
30. Exercício de fixação
Adicione ao store:
- signal privado
searchState; - método
setSearch; computedque combina texto e filtro;- contador de tarefas concluídas;
- seleção que permaneça válida após a busca.
Explique por que o texto de busca é fonte e a lista filtrada é derivada.
31. Desafio individual
Crie um painel de prioridades com:
- tarefas vindas de um estado assíncrono;
- filtro local por prioridade;
- tarefa selecionada dependente da lista;
- métricas derivadas;
- persistência da preferência visual em
localStoragepor efeito; - conversão de uma busca signal para Observable com cancelamento;
- testes das regras puras e do store.
32. Lista de verificação de conclusão
- Sei explicar o que é estado.
- Distingo signal de variável comum e Observable.
- Leio signal com parênteses.
- Sei quando usar
seteupdate. - Não armazeno dados que podem ser derivados.
- Uso
computedpara derivação pura. - Sei por que
linkedSignalcontinua gravável. - Uso
effectapenas para efeito colateral. - Atualizo arrays e objetos imutavelmente.
- Consigo desenhar o grafo de dependências da tela.
33. Critérios de avaliação
| Critério | Pontos |
|---|---|
| Classificação e propriedade do estado | 20 |
Uso correto de signal e imutabilidade |
20 |
Derivações com computed |
20 |
Seleção dependente com linkedSignal |
15 |
| Efeitos colaterais controlados | 10 |
| Integração com template/RxJS | 10 |
| Explicação técnica | 5 |
| Total | 100 |
34. Resumo
- estado é informação mutável relevante para a interface;
- writable signal mantém estado fonte;
computedderiva valores somente leitura, lazy e em cache;linkedSignalmantém estado gravável dependente de outra fonte;effectintegra mudanças com APIs não reativas;- uma fonte de verdade evita inconsistência;
- atualizações imutáveis fornecem novas referências;
- signals e RxJS resolvem problemas diferentes e podem interoperar.
35. Fontes oficiais
- Angular Signals
- Estado dependente com linkedSignal
- Efeitos para APIs não reativas
- Interoperação entre RxJS e signals
Próxima aula
Na Aula 4.9, adicionaremos interceptors, tratamento centralizado de erros e retorno acessível ao fluxo HTTP.