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.
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 anteriorPor 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:
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.
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.
createEffect(() => {
localStorage.setItem('qty', String(qty()));
});O valor devolvido na execução anterior chega como argumento, o que ajuda a comparar:
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.
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/ErrorBoundarymais próximo). - Loops são detectados: effects que disparam um ao outro sem parar lançam erro em vez de travar a página.
batch(() => {
setFirst('Grace');
setLast('Hopper');
}); // effects que leem os dois rodam uma vezLer sem se inscrever
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...).
import { createRoot } from '@velozjs/core';
const dispose = createRoot((dispose) => {
createEffect(() => console.log(count()));
return dispose;
});
dispose(); // o effect paraO 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
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.
function Chart() {
let canvas!: HTMLCanvasElement;
onMount(() => {
const chart = drawChart(canvas);
onCleanup(() => chart.destroy());
});
return <canvas ref={(el) => (canvas = el)} />;
}