Reactivity
Clank tracks reads while a computed value or effect is running. A signal write synchronously invalidates computed values and reruns only the effects that read the changed source. Effects are deduplicated inside a batch.
Clank tracks reads while a computed value or effect is running. A signal write synchronously invalidates computed values and reruns only the effects that read the changed source. Effects are deduplicated inside a batch.
Signals
const count = signal(0, { name: "cart.count" });
count.value; // tracked read
count.get(); // tracked read
count.peek(); // untracked read
count.value = 2; // write
count.set(3); // write and return the new value
count.update(value => value + 1);
const unsubscribe = count.subscribe((next, previous) => {
console.log(previous, "→", next);
});Signals use Object.is equality by default. Supply { equals: false } to notify on every assignment or provide a custom equality function.
Computed values
const subtotal = signal(12);
const taxRate = signal(0.08);
const total = computed(() => subtotal.value * (1 + taxRate.value));A computed value is lazy: it evaluates on its first read, caches the result, and runs again only after a dependency changes and the value is read. Recursive computed evaluation throws a descriptive cycle error.
Effects and cleanup
const stop = effect((cleanup) => {
const controller = new AbortController();
fetch(`/search?q=${query.value}`, { signal: controller.signal });
cleanup(() => controller.abort());
});
stop();The previous cleanup runs before the next effect execution and again when disposed. Effects are synchronous. Use an async resource for request state and stale-result protection instead of making an effect callback itself async.
Batches and transactions
batch(() => {
firstName.value = "Grace";
lastName.value = "Hopper";
}); // dependent effects run once heretransaction() adds rollback semantics:
transaction(() => {
balance.value -= amount;
inventory.value -= 1;
if (inventory.value < 0) throw new Error("Out of stock");
});If the callback throws, every signal written by that transaction is restored before effects flush. Nested transactions merge their journals into the parent on success.
Untracked reads
effect(() => {
console.log(activeId.value, untrack(() => debugSettings.value));
});Changing debugSettings does not rerun this effect.
Ownership
createRoot() owns computed values, effects, resources, and registered cleanup callbacks created inside it. Disposing the root releases all of them in reverse creation order.
const feature = createRoot(dispose => {
const stopPolling = startPolling();
onCleanup(stopPolling);
return { dispose };
});Components automatically receive an owned root; application code usually does not need to create one.
Stores
const state = store({
profile: { name: "Ada" },
tags: ["math"],
});
effect(() => console.log(state.profile.name));
state.profile.name = "Grace";
state.tags.push("compiler");
const serializable = snapshot(state);Stores are lazy deep proxies. Each accessed property gets an independent signal, and object-key iteration has its own dependency. toRaw() returns the original object for an individual proxy; snapshot() recursively returns plain current values.
Async resources
const user = resource(
async (id: string | undefined, { signal }) => {
const response = await fetch(`/api/users/${id}`, { signal });
return response.json();
},
{ immediate: false },
);
await user.reload("42");
user.data.value;
user.status.value; // idle | loading | refreshing | ready | error
user.loading.value;
user.error.value;
user.mutate(current => ({ ...current!, optimistic: true }));
user.abort();Reloading aborts the prior request. Revision tracking also prevents a stale promise from overwriting newer data even when its underlying operation ignores abort signals.
Streams
consumeStream(iterable, initial, reduce) folds an AsyncIterable into a signal. It is useful for token streams, event streams, and progressive server output.