# Aula 4.8 - Signals e estado da interface

## 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`](../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.

```text
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.

```text
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.

```ts
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.

```ts
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.

```ts
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.

```ts
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:

```text
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.

```ts
// 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

```ts
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

```ts
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`

```ts
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

```ts
// 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

```ts
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`

```ts
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.

```ts
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`

```ts
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

```ts
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.

```ts
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`

```ts
// 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`

```ts
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

```ts
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

```ts
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

```ts
@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

```html
<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:

```text
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

```ts
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

```ts
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:

```text
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:

```ts
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](../exemplos/aula-4.8/index.html).

### 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

- [Angular Signals](https://angular.dev/guide/signals)
- [Estado dependente com linkedSignal](https://angular.dev/guide/signals/linked-signal)
- [Efeitos para APIs não reativas](https://angular.dev/guide/signals/effect)
- [Interoperação entre RxJS e signals](https://angular.dev/ecosystem/rxjs-interop)

## Próxima aula

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