Skip to content
Primitives

useScope

Runs a factory function exactly once per mount inside an effect scope. The factory’s return value is stable across re-renders. Reactive subscriptions registered inside the factory (via observe) are automatically cleaned up on unmount.

import {
import observable
observable
,
import observe
observe
,
import onMount
onMount
,
import onUnmount
onUnmount
,
import useScope
useScope
} from "@usels/web";
function
function useCounter(): any
useCounter
() {
return
import useScope
useScope
(() => {
const
const count$: any
count$
=
import observable
observable
(0);
import onMount
onMount
(() => {
any
console
.
any
log
("mounted");
});
import onUnmount
onUnmount
(() => {
any
console
.
any
log
("unmounted");
});
return {
count$: any
count$
};
});
}

Pass a second argument to receive reactive props inside the factory. p.field always returns the latest value without tracking. Use toObs(p) to get a reactive Observable<P> that updates when props change.

import { useScope, toObs, observe } from "@usels/web";
function useThemeSync(props: { theme: string }) {
return useScope((p) => {
// p.theme — raw latest (no reactive tracking)
// obs$.theme.get() — reactive, triggers re-observation on change
const obs$ = toObs(p);
observe(() => {
document.documentElement.dataset.theme = obs$.theme.get();
});
return {};
}, props);
}
APITimingNotes
onBeforeMountuseLayoutEffect — pre-paintDOM measurement, scroll position restore
onMountuseEffect — after mountReturns optional cleanup function
onUnmountcomponent unmountShorthand for onMount(() => cleanup)
import { useScope, onBeforeMount, onMount, onUnmount } from "@usels/web";
useScope(() => {
onBeforeMount(() => {
// runs at useLayoutEffect timing — before paint
});
onMount(() => {
const sub = source$.onChange(handler);
return () => sub(); // cleanup runs on unmount
});
onUnmount(() => {
resource.release(); // cleanup-only
});
});

Use observe from @usels/web (not @legendapp/state) so subscriptions are automatically registered to the current scope and cleaned up on unmount.

import { useScope, observe } from "@usels/web";
useScope(() => {
observe(() => {
// re-runs whenever any accessed observable changes
// automatically disposed when scope is destroyed
document.title = title$.get();
});
});

Pass a hints map as the second argument to toObs for fields that are opaque objects (DOM elements, class instances, Dates, etc.). Supported hints: 'opaque' and 'plain'.

Callback props do not need a hint — dispatch them via raw prop access (p.onClick?.(...)) so every call resolves to the latest closure.

import { useScope, toObs, observe } from "@usels/web";
function useEventHandler(props: { onClick: (e: MouseEvent) => void; data: SomeObject }) {
return useScope((p) => {
const obs$ = toObs(p, {
data: "opaque", // prevents deep-proxying
});
observe(() => {
// raw-prop dispatch — always latest closure
element.addEventListener("click", (e) => p.onClick?.(e));
});
return {};
}, props);
}

In development with Strict Mode, React simulates unmount/remount to detect side-effect bugs. The factory may run twice per mount cycle. This is expected and safe — production always runs the factory once.

When a component is rendered inside a StoreProvider, getStore() works inside a useScope factory without any additional setup.

import { createStore, observable, observe, useScope } from "@usels/web";
const [, getSettingsStore] = createStore("settings", () => {
const theme$ = observable<"light" | "dark">("light");
return { theme$ };
});
function useThemeSync() {
return useScope(() => {
const { theme$ } = getSettingsStore(); // resolves from nearest StoreProvider
observe(() => {
document.documentElement.dataset.theme = theme$.get();
});
return {};
});
}

View on GitHub