Hooks
Every Chroma hook with its dependency-array semantics, and how to write custom hooks.
Chroma's hooks reuse React names but not React semantics: dependencies are getters, and there are no rules of hooks. The reason is that components run once, so there is no "next render". Exact signatures: API reference.
Dependency arrays
The dependency array holds signal getters, not values.
useEffect(() => { /* ... */ }, [count]); // getter: subscribes to the signal
useEffect(() => { /* ... */ }, [count()]); // value: a number cannot be subscribed toA dependency in Chroma is a subscription to a signal. Passing a value gives the runtime a number that never changes.
| Deps | Behavior |
|---|---|
| omitted | Auto-tracking: every signal read inside the function subscribes |
[getterA, getterB] | Subscribes to exactly those signals; the body runs untracked |
[] | Runs once, never re-runs |
Passing a non-function in the array logs chroma: dependencies must be getters (functions) - pass 'count', not 'count()' to the console. It does not throw.
hookState
const [count, setCount] = hookState(0);count()reads the value. Inside a reactive expression it registers a dependency.setCount(next)writes. Subscribed bindings re-run synchronously.setCount((prev) => next)is the updater form.- Writing the same value (
Object.is) is a no-op.
The name is hookState, not useState, to keep it distinct from React's.
useEffect and useLayoutEffect
Both run a side effect when their dependencies change, and both support cleanup by returning a function. Cleanup runs before every re-run.
| First run | Re-runs | |
|---|---|---|
useEffect | In a microtask, after mounting | Synchronous on dependency change |
useLayoutEffect | Synchronous after render mounts, before paint | Synchronous on dependency change |
Use useLayoutEffect when you need the DOM before the first frame (measure an element, focus an input).
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);An error thrown in an effect body or cleanup is caught and logged to the console. See Reactivity.
useMemo
const doubled = useMemo(() => count() * 2, [count]);Returns a getter, not a value. Reading doubled() inside a reactive expression subscribes to the memo's own signal, which gives you derived chains. The computation re-runs only when its dependencies change, never on read. See Derived state.
useCallback
Returns the function you pass in, unchanged. Components run once, so there is nothing to memoize. It exists so React-shaped code ports cleanly.
Do not put a useCallback result in a dependency array. The runtime calls dependencies as getters, which would run the callback.
useRef
const box = useRef(null);
e("div", { ref: box });A plain { current } object with no reactivity. The ref prop accepts a useRef object (sets .current) or a callback (called with the node).
useReducer
const [state, dispatch] = useReducer(reducer, initialState);Returns [getter, dispatch]. dispatch(action) computes reducer(current, action) and writes the result. If the reducer throws, the error is logged and the state is unchanged.
createContext and useContext
const Theme = createContext("light");
function App() {
return e(Theme.Provider, { value: "dark" }, () => e(Toolbar));
}
function Toolbar() {
const theme = useContext(Theme); // "dark"
return e("div", null, theme);
}Provider children must be a function. e(Child) in a plain call evaluates before the Provider runs, outside its context. A function defers evaluation until the Provider has set its value. useContext returns the nearest active Provider's value, or the default.
useDebugValue
globalThis.__CHROMA_DEBUG__ = true;
useDebugValue(value, (v) => `state: ${v}`);Logs through console.debug when the global flag is truthy, applying the optional formatter first. Chroma has no DevTools.
Custom hooks
A custom hook is a function that composes the primitives:
function useCounter(initial) {
const [count, setCount] = hookState(initial);
const doubled = useMemo(() => count() * 2, [count]);
const inc = () => setCount(count() + 1);
return { count, doubled, inc };
}There is no call-order constraint. Each call allocates a new signal or binding, so you can call hooks conditionally or in loops. Be aware that every call also allocates something that lives for the life of the page.