Introduction
The Problem
Section titled “The Problem”In observable/signal-based frameworks like SolidJS, Svelte, and Vue (setup), the component function body runs only once at mount time. When state changes, the framework does not re-execute the entire function — its reactive system propagates only the changes.
React works differently. Every state change re-executes the entire function body. This creates a fundamental friction when writing observable-based code:
function Counter() { // ❌ Every re-render creates a brand-new observable instance // → All subscriptions to the previous instance are lost const count$ = observable(0);
// ❌ A new observe() is set up every render — previous ones are never cleaned up observe(() => { document.title = `Count: ${count$.get()}`; });
return <button onClick={() => count$.set(c => c + 1)}>{count$.get()}</button>;}Because the observable() reference itself is recreated on every re-render, subscriptions are silently destroyed — the intent is to propagate internal value changes, but the reference keeps being replaced. To work around this, React requires hook wrappers like useObservable() that preserve the same observable reference across re-renders.
This gap also causes a code split between store and local state:
// global store — use observable() directlyconst store = createStore(() => { const count$ = observable(0); // ✅ runs once return { count$ };});
// local component — must wrap with useObservable()function Counter() { const count$ = useObservable(0); // hook wrapper required return <button>{count$.get()}</button>;}Store setup runs once, so observable() works directly. But inside a component, you must use useObservable(). Same library, same state model — different APIs depending on context.
What "use scope" Solves
Section titled “What "use scope" Solves”"use scope" turns a component function body into a scope factory that runs only once — just like the setup phase in SolidJS or Svelte. Inside the scope, you can call observable(), observe(), onMount(), and other primitives directly, without hook wrappers.
import { observable, observe, onMount, onUnmount } from "@usels/core";
function Counter() { "use scope";
// ✅ Runs once — the same reference is preserved across re-renders const count$ = observable(0);
observe(() => { document.title = `Count: ${count$.get()}`; });
onMount(() => console.log("mounted")); onUnmount(() => console.log("unmounted"));
return <button onClick={() => count$.set(c => c + 1)}>{count$.get()}</button>;}Store and local component can now use the same code:
// global store — observable() directlyconst store = createStore(() => { const count$ = observable(0); return { count$ };});
// local component — same observable() with "use scope"function Counter() { "use scope"; const count$ = observable(0); // identical to the store code return <button>{count$.get()}</button>;}When Scope Shines
Section titled “When Scope Shines”- Writing observable/signal-based code in a run-once environment inside React.
- Using the same API in both store setup and local components.
- Complex lifecycle coordination inside a single component or store setup.
- Code paths that would otherwise be fragile under hook-ordering rules (conditional setup, loop-driven subscriptions).
Tradeoffs
Section titled “Tradeoffs”- Team-level convention required. Scope code should not interleave with React hooks inside the same function.
- Learning curve for debugging scope disposal.