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.