Engenharia de IA Aplicada à Programação Web
Aula 8 de 10
Navegar por módulos e aulas
  1. 01 Fundamentos da Web
  2. 02 JavaScript moderno
  3. 03 TypeScript, Git e qualidade
  4. 04 Angular
  5. 05 Node.js, NestJS e APIs
  1. 4.1 Introdução ao Angular e componentes
  2. 4.2 Templates, bindings e control flow
  3. 4.3 Composição, inputs e outputs
  4. 4.4 Serviços e injeção de dependência
  5. 4.5 Rotas, layouts e navegação
  6. 4.6 Formulários e validação
  7. 4.7 HTTP, APIs e RxJS
  8. 4.8 Signals e estado da interface
  9. 4.9 Interceptors, erros e acessibilidade
  10. 4.10 Testes, compilação e projeto final

Módulo 4 · Aula 4.8

Signals e estado da interface

2 horas Teoria + laboratório Angular
Ver fonte Markdown

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:

  1. explicar estado, reatividade e fonte de verdade;
  2. distinguir estado de domínio, remoto, local, visual, de formulário e derivado;
  3. criar e ler writable signals;
  4. atualizar valores com set e update;
  5. expor signals somente leitura com asReadonly;
  6. criar valores derivados com computed;
  7. explicar avaliação lazy, cache e dependências dinâmicas;
  8. preservar estado dependente válido com linkedSignal;
  9. usar effect apenas para efeitos colaterais;
  10. reconhecer quando usar untracked e igualdade personalizada;
  11. atualizar arrays e objetos sem mutação invisível;
  12. 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:

  1. signal privado searchState;
  2. método setSearch;
  3. computed que combina texto e filtro;
  4. contador de tarefas concluídas;
  5. 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 localStorage por 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 set e update.
  • Não armazeno dados que podem ser derivados.
  • Uso computed para derivação pura.
  • Sei por que linkedSignal continua gravável.
  • Uso effect apenas 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;
  • computed deriva valores somente leitura, lazy e em cache;
  • linkedSignal mantém estado gravável dependente de outra fonte;
  • effect integra 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

Próxima aula

Na Aula 4.9, adicionaremos interceptors, tratamento centralizado de erros e retorno acessível ao fluxo HTTP.