Skip to content

TigerWest/use-legend

Repository files navigation

use-legend

Fine-grained updates. Zero wasted renders.

use-legend is a set of React state primitives and utilities built on Legend-State. Use it for component-local observables, provider-scoped global stores, and observable-first utility hooks that update the exact UI reads that changed.

Why

  • useObservable() replaces useState with an observable that updates only the JSX leaves that read it — the component function does not re-run.
  • createStore() gives you provider-scoped global stores with observable state, actions, lifecycle cleanup, inter-store access, and SSR-safe boundaries.
  • use* utility hooks cover timers, browser APIs, sensors, sync, and integrations without switching away from the same observable model.

The result is direct DX for stateful React code and a faster UX path: observable reads update fine-grained render boundaries instead of forcing whole components to re-render by default.

Installation

pnpm add @usels/core @legendapp/state@beta @usels/web
pnpm add -D @usels/vite-plugin

Use @usels/core for local state, global stores, reactivity, timers, and sync primitives. Add @usels/web when you need browser, element, or sensor APIs.

Vite Setup

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import useLegend from "@usels/vite-plugin";

export default defineConfig({
  plugins: [useLegend(), react()],
});

Put useLegend() before react(). The plugin wraps observable .get() reads in JSX with fine-grained Memo boundaries — the component renders once, and only the leaf holding .get() updates when the observable changes.

Quick Start

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>
  );
}

Click the button — count$.get() updates but renderCount stays at 1. The component function never re-runs.

Reactive Effects

Use useObserve() to run work whenever an observable changes. Combine with utility hooks like useDebounced():

import { useDebounced } from "@usels/core";
import { useObservable, useObserve } from "@legendapp/state/react";

function ProductSearch({ onSearch }: { onSearch: (query: string) => void }) {
  const draft$ = useObservable("");
  const debounced$ = useDebounced(draft$, { ms: 150 });

  useObserve(() => {
    onSearch(debounced$.get());
  });

  return (
    <input
      value={draft$.get()}
      onChange={(event) => draft$.set(event.currentTarget.value)}
      placeholder="Search products"
    />
  );
}

Global Store

Use createStore() when app or domain state should be shared through a React tree.

import { createStore, observable, StoreProvider } from "@usels/core";

const [useProductStore] = createStore("products", () => {
  const query$ = observable("");
  const cart$ = observable<Record<string, number>>({});

  const cartCount$ = observable(() =>
    Object.values(cart$.get()).reduce((sum, quantity) => sum + quantity, 0)
  );

  const setQuery = (query: string) => query$.set(query);

  const addToCart = (id: string, quantity = 1) => {
    cart$.set((cart) => ({
      ...cart,
      [id]: (cart[id] ?? 0) + quantity,
    }));
  };

  return { query$, cart$, cartCount$, setQuery, addToCart };
});

function App() {
  return (
    <StoreProvider>
      <CartButton />
    </StoreProvider>
  );
}

function CartButton() {
  const { cartCount$, addToCart } = useProductStore();

  return <button onClick={() => addToCart("keyboard")}>Cart {cartCount$.get()}</button>;
}

useProductStore() reads the store from the nearest StoreProvider. StoreProvider owns the store registry for its subtree — that keeps store instances isolated across SSR requests, app shells, tests, or embedded roots.

Mental Model

Need API
Local component state useObservable()
Reactive side effects useObserve(), useWatch(), useWhenever()
Global app or domain state createStore()
Observable utility hooks useDebounced(), useIntervalFn(), ...
Render boundaries Memo, For, or the Vite/Babel transform

Packages

Package Role
@usels/core Store, reactivity, timers, sync primitives, and Legend-State exports
@usels/web Browser, element, and sensor APIs
@usels/tanstack-query Observable-native TanStack Query integration
@usels/vite-plugin Vite transform for JSX .get() reads
@usels/babel-plugin Underlying Babel transform used by the Vite plugin
@usels/eslint-plugin Lint rules for observable naming and render patterns

Links

Development

pnpm install
pnpm build
pnpm test
pnpm lint

Requirements

  • Node >= 18.0.0
  • pnpm >= 8.0.0

Claude Code Skills Plugin

This repository also ships a Claude Code marketplace with one plugin:

/plugin marketplace add TigerWest/use-legend
/plugin install use-legend-functions@use-legend-skills

Includes 5 skills:

  • use-legend-hooks-catalog — reference for 100+ hooks in @usels/core and @usels/web
  • use-legend-create-custom-hook — authoring custom reactive utilities
  • use-legend-scope-components — components with the "use scope" directive
  • use-legend-best-practices — naming, refs, reactive anti-patterns
  • use-legend-testing-best-practices — rerender, lifecycle, reactive options

Regenerate the hook catalog after source changes:

pnpm skills:build

License

MIT

About

Fine-grained React primitives and utility hooks powered by Legend-State.

Resources

Stars

Watchers

Forks

Contributors