Reatividade

Reatividade

Signals guardam valores, memos derivam deles, effects reagem a eles. O Veloz registra quem lê o quê e roda de novo só o que depende de uma mudança.

Signals

Um signal é um getter e um setter. Lê-lo dentro de uma computação (um effect, um memo, uma função no JSX) inscreve essa computação.

ts
import { createSignal } from '@velozjs/core';

const [count, setCount] = createSignal(0);

count();                // 0
setCount(5);            // define um valor
setCount((c) => c + 1); // ou atualiza a partir do anterior

Por padrão, definir o mesmo valor (por Object.is) não faz nada. Passe { equals: false } para notificar sempre, ou sua própria comparação:

ts
const [list, setList] = createSignal<string[]>([], { equals: false });

Memos

Um memo é um valor derivado em cache. Ele recalcula só quando uma das fontes muda, e só notifica quem depende dele quando o resultado é diferente.

ts
const [price, setPrice] = createSignal(24);
const [qty, setQty] = createSignal(2);

const subtotal = createMemo(() => price() * qty());
const shipping = createMemo(() => (subtotal() >= 100 ? 0 : 9));

Aqui, mudar qty de 2 para 3 recalcula subtotal, que recalcula shipping; mas, se shipping continuar 9, nada que leia só shipping roda de novo. O playground da página inicial mostra exatamente isso.

Effects

Um effect roda na hora e de novo sempre que um signal que ele leu muda. Use para efeitos colaterais: log, sincronizar com o localStorage, conversar com uma biblioteca que não é Veloz.

ts
createEffect(() => {
  localStorage.setItem('qty', String(qty()));
});

O valor devolvido na execução anterior chega como argumento, o que ajuda a comparar:

ts
createEffect((previous?: number) => {
  const now = count();
  if (previous !== undefined) console.log(`${previous} → ${now}`);
  return now;
});

Limpeza

onCleanup registra uma função que roda antes de o effect rodar de novo e quando o dono dele é descartado.

ts
createEffect(() => {
  const id = setInterval(tick, delay());
  onCleanup(() => clearInterval(id));
});

Como as atualizações são agendadas

  • Em lote: escritas dentro de batch() (e as feitas enquanto um effect roda) são aplicadas uma vez, no final.
  • Sem estados inconsistentes: quando um signal muda, os dependentes são marcados primeiro e rodam depois, em ordem. Um effect nunca vê um memo desatualizado e roda uma vez mesmo que várias fontes tenham mudado.
  • Erros não travam a fila: se um effect lança erro, os outros rodam mesmo assim; o erro é relançado no fim (ou vai para o catchError / ErrorBoundary mais próximo).
  • Loops são detectados: effects que disparam um ao outro sem parar lançam erro em vez de travar a página.
ts
batch(() => {
  setFirst('Grace');
  setLast('Hopper');
}); // effects que leem os dois rodam uma vez

Ler sem se inscrever

ts
import { untrack, on } from '@velozjs/core';

createEffect(() => {
  // roda de novo quando `query` muda, não quando `page` muda
  search(query(), untrack(page));
});

// Dependências explícitas; o callback roda sem rastreamento.
createEffect(on(query, (value, previous) => console.log(previous, '→', value), { defer: true }));
createEffect(on([first, last], ([f, l]) => console.log(f, l)));

Ownership

Toda computação pertence a um dono: o componente, effect ou raiz que a criou. Descartar um dono descarta tudo o que ele possui, então os effects de um componente param quando ele sai da tela (um ramo do Show que vira falso, uma linha removida do For...).

ts
import { createRoot } from '@velozjs/core';

const dispose = createRoot((dispose) => {
  createEffect(() => console.log(count()));
  return dispose;
});

dispose(); // o effect para

O createRoot cria um escopo que não é descartado com o pai. Use para stores de vida longa ou para código fora de componentes.

Contexto

tsx
const Theme = createContext<'light' | 'dark'>('light');

function App() {
  return <Theme.Provider value="dark">{() => <Toolbar />}</Theme.Provider>;
}

function Toolbar() {
  const theme = useContext(Theme); // 'dark'
  return <div class={theme}>…</div>;
}

Passe os filhos do Provider como função para que sejam criados dentro dele (veja Componentes e JSX).

Ciclo de vida

onMount(fn) roda uma vez depois que os nós do componente são criados (numa microtask), dentro do dono do componente, então onCleanup funciona nele. Não roda se o componente for descartado antes.

tsx
function Chart() {
  let canvas!: HTMLCanvasElement;
  onMount(() => {
    const chart = drawChart(canvas);
    onCleanup(() => chart.destroy());
  });
  return <canvas ref={(el) => (canvas = el)} />;
}