Reactivity: Signals, Effects & Memos
Krate's reactivity system is inspired by SolidJS: fine-grained, signal-based, with no virtual DOM and no diffing.
All primitives are exported from @krate/runtime.
Signals
import { createSignal } from '@krate/runtime'; const [count, setCount] = createSignal(0); count()reads the current value.setCount(next)sets the value and notifies subscribers.setCount(prev => ...)performs a functional update.
Signals are read inside components and effects; the compiler and runtime track which effects depend on which signals automatically.
Effects
import { createEffect } from '@krate/runtime'; const dispose = createEffect(() => { console.log(\`count is \${count()}\`); }); dispose(); // remove the effect An effect re-runs whenever any signal it reads changes. The tracker works via a context stack, so nested reads inside createEffect are captured correctly.
Memos
import { createMemo } from '@krate/runtime'; const doubled = createMemo(() => count() * 2); doubled(); // memoized until count changes A memo caches its computed value and only recomputes when its dependencies change. Only the final consumer re-renders — the memo itself is cached.
Cleanup & mount
import { onCleanup, onMount } from '@krate/runtime'; createEffect(() => { const timer = setInterval(() => {}, 1000); onCleanup(() => clearInterval(timer)); // runs before re-run and on dispose }); onMount(() => { // runs once after the initial synchronous render completes }); Context
import { createContext } from '@krate/runtime'; const ThemeCtx = createContext('light'); // ThemeCtx.Provider — wraps children with a value // ThemeCtx.useContext() — reads the nearest provider value // ThemeCtx.defaultValue — fallback when no provider is present Resources
import { createResource } from '@krate/runtime'; const [user, { mutate, refetch }] = createResource( () => userId(), // reactive source async (id) => fetch(\`/api/user/\${id}\`).then(r => r.json()), // fetcher ); user() // current data (or undefined) user.loading // boolean user.error // error or undefined user.state // 'unresolved' | 'loading' | 'ready' | 'error' | 'refreshing' mutate(prev => ({ ...prev, name: 'new' })) // optimistic update refetch() // trigger a re-fetch Resources re-fetch automatically when their source signal changes and abort in-flight requests when the source changes mid-flight.
Compile-time validation
The reactive dependency graph is validated at build time and surfaced as ⚠ warnings. The compiler detects:
- Unused signals — declared but never read or written.
- Write-only effects — effects that call setters but read no signals.
- No-read effects — effects that read and write nothing (run exactly once).
- Circular dependencies — including self-referential feedback loops like
setCount(count() + 1).
These warnings help you catch bugs before they reach the browser.