Signals
Signals are reactive state containers. When a signal updates, only the parts of the DOM that depend on it re-render. There's no diff, no component re-run, no virtual DOM.
Two flavors
Flexium ships two ways to create signals:
use() — hooks-shaped (value form)
import { use } from 'flexium/core'
function Counter() {
const [count, setCount] = use(0)
return <button onclick={() => setCount(c => c + 1)}>{count}</button>
}countis the current value at the point of render (not a function call).setCountaccepts a new value or an updater(prev) => next.- Must be called inside a component function (hook rules).
createSignal() — getter form (escape hatch)
import { createSignal } from 'flexium/core'
const [count, setCount] = createSignal(0)
console.log(count()) // 0 ← getter is a function
setCount(5)
console.log(count()) // 5countis a getter function. Call it to read.- Works anywhere — module scope, inside async code, outside components.
- Per-call cost ~22ns (vs
use()'s 550ns runtime / 13ns compile-time).
Use use() for normal components. Drop to createSignal() for hot loops, animation frames, or hundreds of signals per component.
Derived state
The functional form of either creates a derived signal — auto-tracked, lazy, cached:
const [count, setCount] = use(0)
const [doubled] = use(() => count * 2) // derives from count
const [isEven] = use(() => count % 2 === 0) // derives from count
// or with createSignal
import { createComputed } from 'flexium/core'
const doubledG = createComputed(() => count() * 2)doubled only re-evaluates when count changes. Equivalent to React's useMemo but with fine-grained tracking — you don't pass a [deps] array, the runtime figures it out.
Effects (side effects)
import { unsafeEffect } from 'flexium/core'
function Logger() {
const [count, setCount] = use(0)
// Runs after every count change. Auto-disposed on unmount.
unsafeEffect(() => {
console.log('count is now', count)
})
return <button onclick={() => setCount(c => c + 1)}>{count}</button>
}For non-component contexts, use createEffect(fn) — same behavior, no auto-cleanup (returns dispose function).
Batched updates
Multiple set calls in the same synchronous block coalesce into a single flush:
function handleClick() {
setA(1)
setB(2)
setC(3)
// → 1 microtask flush, 1 DOM patch pass for all three
}For forcing a flush mid-handler:
import { sync } from 'flexium/core'
function handleClick() {
setA(1)
sync() // flushes pending updates synchronously
setB(2) // separate flush
}Shared state across components
Pass { key } to share a signal across the tree without a Provider:
function ThemeToggle() {
const [theme, setTheme] = use<'light' | 'dark'>('light', { key: 'theme' })
return <button onclick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>{theme}</button>
}
function Header() {
// Same key — automatically subscribed to the same signal
const [theme] = use<'light' | 'dark'>('light', { key: 'theme' })
return <header data-theme={theme}>...</header>
}When the last component using a key unmounts, the signal is released.
Async resources
use(async fn) creates a Promise-aware signal:
function UserProfile({ id }) {
const [user] = use(async () => {
const res = await fetch(`/api/users/${id}`)
return res.json()
})
// Pause render until promise resolves
return <h1>{user.name}</h1>
}
// Wrap in Suspense for loading UI
<Suspense fallback={<p>Loading…</p>}>
<UserProfile id={1} />
</Suspense>The async function captures signals it reads. When a captured signal changes, the function re-runs.
Tracking rules
- Reading a signal inside a render function, effect, or derived signal subscribes the consumer.
- Reading a signal outside any reactive context (e.g., in an event handler) does not subscribe — it's a plain read.
setSignal(value)updates the signal and schedules a flush.
Next
→ Components — JSX, props, children, refs.