Introduction
Use Legend-State as the first state model in app code.
Observable-First Rules
Section titled “Observable-First Rules”- No
useState - No
useReducer - No component-wide re-rendering as the default update path
Instead, keep mutable values in observables and update only the UI nodes that depend on those values. Hooks are still useful, but mainly for composing side effects, timers, sensors, and reusable logic around observables.
Why hooks + observables work well together
Section titled “Why hooks + observables work well together”When an interval updates a regular React state value, the whole component re-renders. When the interval updates an observable, only the bound text node updates and the parent component render count stays stable.
Normal
Renders: 1 (state-based card)
Count: 1
Fine-grained
Renders: 1 (observable-based card)
Count: 1
Show code
import { useObservable } from "@legendapp/state/react";
import { useIntervalFn } 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);
}, 24);
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);
}, 24);
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.