{"protocol":"clank-doc/1","frameworkVersion":"0.8.0","slug":"reactivity","title":"Reactivity","description":"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.","group":{"id":"framework","title":"Framework"},"url":"https://docs.clank.run/docs/reactivity","source":"docs/reactivity.md","headings":["Signals","Computed values","Effects and cleanup","Batches and transactions","Untracked reads","Ownership","Stores","Async resources","Streams"],"tableOfContents":[{"id":"signals","title":"Signals","level":2},{"id":"computed-values","title":"Computed values","level":2},{"id":"effects-and-cleanup","title":"Effects and cleanup","level":2},{"id":"batches-and-transactions","title":"Batches and transactions","level":2},{"id":"untracked-reads","title":"Untracked reads","level":2},{"id":"ownership","title":"Ownership","level":2},{"id":"stores","title":"Stores","level":2},{"id":"async-resources","title":"Async resources","level":2},{"id":"streams","title":"Streams","level":2}],"markdown":"# Reactivity\n\nClank 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.\n\n## Signals\n\n```ts\nconst count = signal(0, { name: \"cart.count\" });\n\ncount.value;                     // tracked read\ncount.get();                     // tracked read\ncount.peek();                    // untracked read\ncount.value = 2;                 // write\ncount.set(3);                    // write and return the new value\ncount.update(value => value + 1);\n\nconst unsubscribe = count.subscribe((next, previous) => {\n  console.log(previous, \"→\", next);\n});\n```\n\nSignals use `Object.is` equality by default. Supply `{ equals: false }` to notify on every assignment or provide a custom equality function.\n\n## Computed values\n\n```ts\nconst subtotal = signal(12);\nconst taxRate = signal(0.08);\nconst total = computed(() => subtotal.value * (1 + taxRate.value));\n```\n\nA 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.\n\n## Effects and cleanup\n\n```ts\nconst stop = effect((cleanup) => {\n  const controller = new AbortController();\n  fetch(`/search?q=${query.value}`, { signal: controller.signal });\n  cleanup(() => controller.abort());\n});\n\nstop();\n```\n\nThe 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.\n\n## Batches and transactions\n\n```ts\nbatch(() => {\n  firstName.value = \"Grace\";\n  lastName.value = \"Hopper\";\n}); // dependent effects run once here\n```\n\n`transaction()` adds rollback semantics:\n\n```ts\ntransaction(() => {\n  balance.value -= amount;\n  inventory.value -= 1;\n  if (inventory.value < 0) throw new Error(\"Out of stock\");\n});\n```\n\nIf the callback throws, every signal written by that transaction is restored before effects flush. Nested transactions merge their journals into the parent on success.\n\n## Untracked reads\n\n```ts\neffect(() => {\n  console.log(activeId.value, untrack(() => debugSettings.value));\n});\n```\n\nChanging `debugSettings` does not rerun this effect.\n\n## Ownership\n\n`createRoot()` owns computed values, effects, resources, and registered cleanup callbacks created inside it. Disposing the root releases all of them in reverse creation order.\n\n```ts\nconst feature = createRoot(dispose => {\n  const stopPolling = startPolling();\n  onCleanup(stopPolling);\n  return { dispose };\n});\n```\n\nComponents automatically receive an owned root; application code usually does not need to create one.\n\n## Stores\n\n```ts\nconst state = store({\n  profile: { name: \"Ada\" },\n  tags: [\"math\"],\n});\n\neffect(() => console.log(state.profile.name));\nstate.profile.name = \"Grace\";\nstate.tags.push(\"compiler\");\n\nconst serializable = snapshot(state);\n```\n\nStores 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.\n\n## Async resources\n\n```ts\nconst user = resource(\n  async (id: string | undefined, { signal }) => {\n    const response = await fetch(`/api/users/${id}`, { signal });\n    return response.json();\n  },\n  { immediate: false },\n);\n\nawait user.reload(\"42\");\nuser.data.value;\nuser.status.value;  // idle | loading | refreshing | ready | error\nuser.loading.value;\nuser.error.value;\nuser.mutate(current => ({ ...current!, optimistic: true }));\nuser.abort();\n```\n\nReloading aborts the prior request. Revision tracking also prevents a stale promise from overwriting newer data even when its underlying operation ignores abort signals.\n\n## Streams\n\n`consumeStream(iterable, initial, reduce)` folds an `AsyncIterable` into a signal. It is useful for token streams, event streams, and progressive server output.\n"}