Scope & Lifecycle
A scope is a lifecycle boundary for observable state, effects, and cleanup. Use it when state belongs to one component or one custom hook.
”use scope”
Section titled “”use scope””With the Vite or Babel plugin enabled, the "use scope" directive is compiled
to useScope(...).
import { observable, observe, onMount, onUnmount } from "@usels/core";
function Counter() { "use scope";
const count$ = observable(0); const increment = () => count$.set((count) => count + 1);
observe(() => { document.title = `Count ${count$.get()}`; });
onMount(() => { console.log("mounted"); });
onUnmount(() => { console.log("unmounted"); });
return <button onClick={increment}>{count$.get()}</button>;}The observable and functions are created once for the component mount. Scope effects are disposed when the component unmounts.
Props In Scoped Components
Section titled “Props In Scoped Components”When a scoped component needs reactive prop tracking, keep the component props as
an identifier and pass that identifier to toObs().
import { observable, observe, toObs } from "@usels/core";
type EditableTitleProps = { value: string; onCommit: (value: string) => void;};
function EditableTitle(props: EditableTitleProps) { "use scope";
const props$ = toObs(props, { onCommit: "function" }); const draft$ = observable(props.value);
observe(() => { draft$.set(props$.value.get()); });
const commit = () => { props$.onCommit.get()(draft$.peek()); };
return ( <input value={draft$.get()} onChange={(event) => draft$.set(event.currentTarget.value)} onBlur={commit} /> );}Inside the scope body, props.value is a latest-value read that does not track.
props$.value.get() is reactive and re-runs the observe() callback when the
parent passes a new value.
Use hints for non-plain props such as functions, opaque objects, and React
elements. In the example, { onCommit: "function" } keeps the callback as a
function value inside the observable props object.
Why toObs() Instead of get()
Section titled “Why toObs() Instead of get()”The get() utility from @usels/core normalizes DeepMaybeObservable values, but it is a plain function call. The Babel plugin only detects .get() method calls (MemberExpression) to create reactive Memo boundaries. Using get(props).field in JSX will not trigger autoWrap — no fine-grained reactivity.
toObs() converts props into an Observable object, so you can use .get() method calls that the plugin detects:
// ❌ get() function — plugin cannot detect, no Memo boundary<h1>{get(props).title}</h1>
// ✅ toObs() + .get() method — plugin auto-wraps"use scope"const props$ = toObs(props);<h1>{props$.title.get()}</h1>useScope
Section titled “useScope”Use useScope directly when you need explicit props handling or when a build
pipeline cannot use the directive transform.
import { observable, observe, toObs, useScope } from "@usels/core";
function useThemeSync(props: { theme: string }) { return useScope((p) => { const props$ = toObs(p);
observe(() => { document.documentElement.dataset.theme = props$.theme.get(); });
return {}; }, props);}Plain p.theme reads always give the latest value without tracking. Use
toObs(p) when the scope needs reactive prop tracking.
Scope-Aware APIs
Section titled “Scope-Aware APIs”Inside a scope, use the scope-aware lifecycle APIs from @usels/core:
| Need | API |
|---|---|
| DOM measurement before paint | onBeforeMount() |
| Work after mount | onMount() |
| Cleanup on unmount | onUnmount() |
| Reactive props | toObs() |
For reactive side effects inside a scope, see Effects API — the canonical reference for observe(), watch(), and whenever().
Picking use* vs create* Inside Scope
Section titled “Picking use* vs create* Inside Scope”Inside a scope, prefer create* primitives over use* hook wrappers — the scope already owns lifecycle, so the hook rules do not apply. See use* vs create* for the full selection rule and examples.