Targ Apps Docs
Chroma

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 to

A dependency in Chroma is a subscription to a signal. Passing a value gives the runtime a number that never changes.

DepsBehavior
omittedAuto-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 runRe-runs
useEffectIn a microtask, after mountingSynchronous on dependency change
useLayoutEffectSynchronous after render mounts, before paintSynchronous 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.

Next

On this page