Getting Started
Install
Section titled “Install”npm add @usels/core @legendapp/state@beta @usels/webnpm add -D @usels/vite-pluginyarn add @usels/core @legendapp/state@beta @usels/webyarn add -D @usels/vite-pluginpnpm add @usels/core @legendapp/state@beta @usels/webpnpm add -D @usels/vite-pluginbun add @usels/core @legendapp/state@beta @usels/webbun add -D @usels/vite-pluginUse @usels/core for local state, reactivity, timers, stores, and sync primitives. Add @usels/web when you need browser, element, or sensor APIs.
Configure Vite
Section titled “Configure Vite”import { defineConfig } from "vite";import react from "@vitejs/plugin-react";import useLegend from "@usels/vite-plugin";
export default defineConfig({ plugins: [useLegend(), react()],});Place useLegend() before react(). It runs before JSX is compiled.
If you are not using Vite, install the Babel plugin and configure it instead:
npm add -D @usels/babel-pluginyarn add -D @usels/babel-pluginpnpm add -D @usels/babel-pluginbun add -D @usels/babel-pluginmodule.exports = { plugins: ["@usels/babel-plugin"],};See Babel / Next.js for non-Vite build pipelines.
Your First Observable
Section titled “Your First Observable”useObservable() replaces useState for values that should update without re-rendering the whole component.
import { useObservable } from "@legendapp/state/react";
function Counter() { const count$ = useObservable(0); const increment = () => count$.set((value) => value + 1);
return <button onClick={increment}>Clicked {count$.get()} times</button>;}count$ is created once on mount. When it changes, only the text that reads count$.get() updates — the component function itself does not re-run.
Why .get() in JSX?
Section titled “Why .get() in JSX?”Calling count$.get() inside JSX looks like a plain read, but the Babel plugin rewrites each such call into a fine-grained memoized leaf. The component function renders once; only the leaf holding .get() updates when the observable changes.
This is why you rarely need <Computed>, useSelector, or observer(...) wrappers — the plugin handles the tracking. For the setup and edge cases, see Auto-Tracking & .get().
Add Reactive Effects
Section titled “Add Reactive Effects”Use useObserve() to run a callback whenever any observable read inside it changes. Combine it with utility hooks like useDebounced() to build real-world behavior without extra plumbing.
import { useObservable, useObserve } from "@legendapp/state/react";import { useDebounced } from "@usels/core";
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" /> );}useObserve() tracks any observable reads inside its callback and re-runs when they change. useDebounced() returns a new observable that reflects the source after a quiet period — plug it into effects that talk to external systems.
Track Props Reactively
Section titled “Track Props Reactively”Wrap props with useMaybeObservable when a component needs reactive reads of prop values inside useObserve or a derived observable. The return is a DeepMaybeObservable<T> — you can keep using .get() chains.
import { useMaybeObservable } from "@usels/core";import { useObservable, useObserve } from "@legendapp/state/react";
function SearchInput(props: { initialQuery: string; onSearch: (query: string) => void }) { const props$ = useMaybeObservable(props); const draft$ = useObservable(props.initialQuery);
useObserve(() => { draft$.set(props$.initialQuery.get()); });
useObserve(() => { props$.onSearch.get()?.(draft$.get()); });
return ( <input value={draft$.get()} onChange={(event) => draft$.set(event.currentTarget.value)} placeholder="Search products" /> );}Plain props.initialQuery reads stay available as latest-value reads. Use props$.initialQuery.get() when the effect should track prop changes reactively.
Promote Shared State To A Store
Section titled “Promote Shared State To A Store”Use createStore() when state needs a provider boundary and shared access across multiple components.
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. Use StoreProvider to isolate store instances for SSR, tests, embedded roots, and app boundaries.
Combine Store And Local State
Section titled “Combine Store And Local State”A common pattern is to keep fast-changing draft state local and sync the stable result into a global store.
import { useDebounced } from "@usels/core";import { useObservable, useObserve } from "@legendapp/state/react";
function StoreBackedSearch() { const { setQuery } = useProductStore(); const draft$ = useObservable(""); const query$ = useDebounced(draft$, { ms: 150 });
useObserve(() => { setQuery(query$.get()); });
return ( <input value={draft$.get()} onChange={(event) => draft$.set(event.currentTarget.value)} placeholder="Search products" /> );}This keeps keystroke-level UI state local to the component while the shared store receives debounced domain state.
What To Use Next
Section titled “What To Use Next”| Need | Use |
|---|---|
| Observable-first rendering model | Observable-First Mental Model |
How .get() tracking works | Auto-Tracking & .get() |
| Fine-grained rendering | Rendering Boundaries |
| Derived values and effects | Derived State & Effects |
| Browser, element, and sensor state | Reactive Refs & Web Targets |
| Data fetching integration | Data Fetching |