Your own composables
In plain words
A composable is a plain function that creates live things — state, subscriptions, DOM listeners — and cleans up after itself. This page is the naming convention (createX, not useX) and the three-line anatomy every composable shares.
Two kinds of functions, two kinds of names
Pure helpers take values and return values. No lifecycle, no scope — call them anywhere, name them like any function:
const total = (items: State<Item[]>) =>
items.map((xs) => xs.reduce((s, i) => s + i.price, 0));Composables create living things and register cleanup (onCleanup, onMount inside). They must be called during component build, so the name carries a warning label: the create prefix.
const size = createWindowSize(); // "create" = call once, get live wiringWhy createX and not useX
React's use* prefix exists to enforce the Rules of Hooks: fixed call order, top level only, re-run on every render. None of those rules exist here — a component runs once, composables may be called in an if, in a loop, in any order. Borrowing the use* prefix would borrow the wrong mental model along with it. create says exactly what happens: called once, creates something alive.
(This matches Solid's convention, for the same reason.)
Anatomy: create → bridge → clean up
Every composable is the same three moves:
import { state, type State } from "@continuum-js/frp";
import { onCleanup } from "@continuum-js/dom";
export function createWindowSize(): State<{ w: number; h: number }> {
// 1. create the live value
const size = state({ w: innerWidth, h: innerHeight });
// 2. bridge the outside world into the network (at the boundary!)
const onResize = () => size.set({ w: innerWidth, h: innerHeight });
window.addEventListener("resize", onResize);
// 3. register the teardown — the ownership tree calls it on unmount
onCleanup(() => window.removeEventListener("resize", onResize));
return size;
}Use it like any value:
function StatusBar() {
const size = createWindowSize();
return <footer>{size.map((s) => `${s.w}×${s.h}`)}</footer>;
}A realistic second example — the persisted store from the patterns page, packaged:
import { stream } from "@continuum-js/frp";
import { onCleanup } from "@continuum-js/dom";
import { persist, loadPersisted } from "@continuum-js/std";
export function createPersistedTodos(key: string) {
const actions = stream<Action>();
const todos = actions.accum<Todo[]>(loadPersisted(key, []), reduce);
onCleanup(persist(key, todos));
return { todos, dispatch: actions.fire };
}The component shrinks to const { todos, dispatch } = createPersistedTodos("todos").
The one rule
Call a composable synchronously during component build (or inside root()/scope()), because that is when an owner exists to attach the cleanup to — and everything stateful a composable creates (hold, accum, .on, perform) requires that owner. Calling one from a handler or a setTimeout throws a teaching error — there would be no one to ever run the teardown.
That is the whole discipline. No call-order requirements, no "top level only", no dependency arrays. Conditionals are fine:
function Widget(props: { live: boolean }) {
// perfectly legal — this is NOT React
const data = props.live ? createPolling("/api") : constant(null);
...
}Testing a composable
root() gives you an owner outside any component:
import { root } from "@continuum-js/dom";
test("createWindowSize tracks resizes", () => {
root((dispose) => {
const size = createWindowSize();
window.dispatchEvent(new Event("resize"));
expect(size.sample().w).toBe(window.innerWidth);
dispose(); // runs the composable's cleanup
});
});