Introduction
use-legend is an observable-first React utility layer built on Legend-State. State lives in observables, React renders the tree, and fine-grained boundaries update only the UI reads that changed.
At a Glance
Section titled “At a Glance”import { useRef } from "react";import { useObservable } from "@legendapp/state/react";
function Counter() { const count$ = useObservable(0); const renders = useRef(0); renders.current += 1;
return ( <div> <button onClick={() => count$.set((c) => c + 1)}> Clicked {count$.get()} times </button> <p>renderCount: {renders.current}</p> </div> );}Clicking the button increments count$ and updates the text inside the button. But renderCount stays at 1 forever — the component function does not re-run. Only the JSX leaves that read count$.get() are updated. That is fine-grained reactivity in practice.
Three Rules
Section titled “Three Rules”- No
useState— keep mutable values in observables. - No
useReducer— use observable setters or scoped functions. - No component-wide re-rendering as the default update path — bind reads at the leaf.
Hooks are still useful — for composing side effects, timers, sensors, and reusable logic around observables.
See It Live
Section titled “See It Live”Normal
Renders: 1 (state-based card)
Count: 1
Fine-grained
Renders: 1 (observable-based card)
Count: 1
Show code
import { useIntervalFn, useObservable } from "@usels/core";
import { useRef, useState } from "react";
const CARD_BASE_CLASS =
"m-0 flex min-w-[15rem] flex-col gap-2 rounded-[10px] border bg-sl-bg p-3.5";
const CARD_META_CLASS = "text-sm text-sl-text-accent";
function useRenderCount() {
const renders = useRef(0);
renders.current += 1;
return renders.current;
}
function StateDrivenCard() {
const [count, setCount] = useState(1);
const renderCount = useRenderCount();
useIntervalFn(() => {
setCount((v) => v + 1);
}, 500);
return (
<div className={`${CARD_BASE_CLASS} border-orange-300`}>
<h5 className="m-0">Normal</h5>
<div className={CARD_META_CLASS}>
Renders: <strong>{renderCount}</strong> (state-based card)
</div>
<div className="text-lg font-bold">Count: {count}</div>
</div>
);
}
function ObservableDrivenCard() {
const count$ = useObservable(1);
const renderCount = useRenderCount();
useIntervalFn(() => {
count$.set((v) => v + 1);
}, 500);
return (
<div className={`${CARD_BASE_CLASS} border-green-300`}>
<h5 className="m-0">Fine-grained</h5>
<div className={CARD_META_CLASS}>
Renders: <strong>{renderCount}</strong> (observable-based card)
</div>
<div className="text-lg font-bold">Count: {count$.get()}</div>
</div>
);
}
export default function ObservableFirstDemo() {
return (
<div className="grid grid-cols-[repeat(auto-fit,minmax(15rem,1fr))] gap-3">
<StateDrivenCard />
<ObservableDrivenCard />
</div>
);
} Use this pattern for high-frequency updates (timers, polling, streaming, sensors) to keep React work minimal while keeping logic composable with hooks.
What You’ll Build
Section titled “What You’ll Build”After the Learn path you can:
- Build local interactive UI without
useState. - Share state across components with zero wasted re-renders.
- Co-locate effects, timers, and cleanup with scope lifecycle.
Start Here
Section titled “Start Here”- Getting Started → — install and first examples.
- Observable-First Mental Model — the reactivity model.
- Auto-Tracking &
.get()— how.get()in JSX becomes a fine-grained reactive leaf.