Ownership — createRoot, onCleanup, getOwner, runWithOwner#
The disposal tree behind islands, stores and foreign roots: effects created inside a scope are torn down with it, so nothing leaks when a component leaves the page.
import { createRoot, onCleanup, getOwner, runWithOwner } from 'janux';createRoot(fn)#
createRoot<T>(fn: (dispose: () => void) => T): T runs fn inside a fresh scope and hands it the scope's dispose. Every watch and computed created within registers automatically.
import { signal, watch, createRoot } from 'janux';
const count = signal(0);
const stop = createRoot((dispose) => {
watch(() => console.log(count.value));
return dispose;
});
count.value = 1; // logs
stop();
count.value = 2; // silent — the effect went with the scopeNested roots cascade. A root created inside another root is disposed by its parent, so one teardown at the top cleans the whole tree.
onCleanup(fn)#
onCleanup(fn: () => void): void registers an arbitrary teardown in the current scope. Callbacks run in reverse registration order (last registered, first cleaned — the usual "unwind" semantics), and dispose is idempotent.
createRoot((dispose) => {
const socket = openSocket();
onCleanup(() => socket.close());
onCleanup(() => log('closing')); // runs BEFORE socket.close()
return dispose;
});Called on an already-disposed scope, fn runs immediately rather than being silently dropped — late registrations can't leak.
getOwner() / runWithOwner(owner, fn)#
getOwner(): Owner | null captures the current scope; runWithOwner<T>(scope: Owner | null, fn: () => T): T runs work inside it. Together they carry ownership across an await, where the ambient scope is already gone:
const owner = getOwner();
const data = await fetch(url).then((response) => response.json());
runWithOwner(owner, () => {
watch(() => render(data)); // registered in the original scope, disposed with it
});Pass null to run deliberately outside any scope (nothing registers, nothing is disposed for you).
Why it matters for islands#
Island teardown is one dispose() on the island's root: effects stop, cleanups unwind, foreign React roots unmount, subscriptions drop. That is what makes navigation safe — an island that leaves a page leaves nothing behind. See Navigation for what survives a swap and what doesn't.
Related: signal · watch · Foreign-UI interop