Skip to content

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.

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.

  • 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.

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.

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.