Legend State
Legend-State high-performance observable state for React — fine-grained reactivity, persistence plugins, computed observables, and sync engine
You are an expert in using Legend-State for application state management in React and React Native. ## Key Points - **Fine-grained reactivity** — components re-render only when the exact observed value changes - **Zero boilerplate** — get/set with `.get()` and `.set()`, or use object-style assignment - **Built-in persistence** — plug in localStorage, IndexedDB, or AsyncStorage with one call - **Sync engine** — built-in CRUD sync with conflict resolution for offline-first patterns - **Tiny and fast** — benchmarks among the fastest React state libraries - **Framework support** — works with React, React Native, and vanilla JS - **Use `observer()` on all components that read observables** — without it, `.get()` calls will read the value but the component will not re-render on changes. - **Prefer fine-grained access** — read `state$.user.name.get()` rather than `state$.user.get().name` so the component only subscribes to the `name` field. - **Use `batch()` for multi-field updates** — batching prevents intermediate re-renders when setting several values at once. - **Forgetting `observer()` wrapper** — components will read stale values and never re-render. Every component that calls `.get()` must be wrapped with `observer()` or use the `<Memo>` component. - **Reading too broadly with `.get()` on a parent object** — calling `state$.user.get()` subscribes to every field in `user`. Access the specific leaf node instead to keep re-renders minimal. ## Quick Example ```bash npm install @legendapp/state @legendapp/state/react ```
skilldb get state-management-skills/Legend StateFull skill: 259 linesLegend-State — High-Performance Observable State Management
You are an expert in using Legend-State for application state management in React and React Native.
Core Philosophy
Overview
Legend-State is a performance-focused state library built on fine-grained observables. It tracks exactly which properties each component accesses and re-renders only when those specific values change — not when the parent object changes. It ships with built-in persistence (localStorage, IndexedDB, AsyncStorage) and a sync engine for offline-first apps.
- Fine-grained reactivity — components re-render only when the exact observed value changes
- Zero boilerplate — get/set with
.get()and.set(), or use object-style assignment - Built-in persistence — plug in localStorage, IndexedDB, or AsyncStorage with one call
- Sync engine — built-in CRUD sync with conflict resolution for offline-first patterns
- Tiny and fast — benchmarks among the fastest React state libraries
- Framework support — works with React, React Native, and vanilla JS
Setup & Configuration
npm install @legendapp/state @legendapp/state/react
// store/appStore.ts
import { observable } from '@legendapp/state';
interface Todo {
id: string;
text: string;
completed: boolean;
}
export const appState$ = observable({
user: {
name: '',
email: '',
isLoggedIn: false,
},
todos: [] as Todo[],
settings: {
theme: 'light' as 'light' | 'dark',
language: 'en',
},
});
// components/UserGreeting.tsx
import { observer } from '@legendapp/state/react';
import { appState$ } from '../store/appStore';
// observer() HOC enables fine-grained tracking
export const UserGreeting = observer(function UserGreeting() {
// .get() reads the value and subscribes to changes
const name = appState$.user.name.get();
return <h1>Hello, {name || 'Guest'}</h1>;
});
Core Patterns
Reading and Writing Observables
import { appState$ } from '../store/appStore';
// Read a value
const theme = appState$.settings.theme.get();
// Set a value
appState$.settings.theme.set('dark');
// Set nested values
appState$.user.set({
name: 'Alice',
email: 'alice@example.com',
isLoggedIn: true,
});
// Peek without subscribing (no reactivity tracking)
const peeked = appState$.user.name.peek();
// Toggle a boolean
appState$.user.isLoggedIn.toggle();
Fine-Grained Components with observer
import { observer } from '@legendapp/state/react';
import { appState$ } from '../store/appStore';
// This component only re-renders when settings.theme changes,
// even if user or todos change
export const ThemeToggle = observer(function ThemeToggle() {
const theme = appState$.settings.theme.get();
return (
<button onClick={() => appState$.settings.theme.set(theme === 'light' ? 'dark' : 'light')}>
Theme: {theme}
</button>
);
});
// This component only re-renders when todos array changes
export const TodoCount = observer(function TodoCount() {
const count = appState$.todos.get().length;
return <span>{count} todos</span>;
});
Computed Observables
import { observable, computed } from '@legendapp/state';
const cart$ = observable({
items: [] as { name: string; price: number; qty: number }[],
couponDiscount: 0,
});
// Computed values re-evaluate only when dependencies change
const subtotal$ = computed(() =>
cart$.items.get().reduce((sum, item) => sum + item.price * item.qty, 0)
);
const total$ = computed(() =>
Math.max(0, subtotal$.get() - cart$.couponDiscount.get())
);
Observing Changes with onChange
import { appState$ } from '../store/appStore';
// React to specific value changes
const dispose = appState$.user.isLoggedIn.onChange((isLoggedIn) => {
if (!isLoggedIn) {
// Clear sensitive data on logout
appState$.todos.set([]);
}
});
// Cleanup when done
dispose();
Persistence with localStorage
import { observable } from '@legendapp/state';
import { persistObservable } from '@legendapp/state/persist';
import { ObservablePersistLocalStorage } from '@legendapp/state/persist-plugins/local-storage';
const settings$ = observable({
theme: 'light' as 'light' | 'dark',
language: 'en',
notifications: true,
});
// Automatically persists to localStorage under key "app-settings"
persistObservable(settings$, {
local: 'app-settings',
pluginLocal: ObservablePersistLocalStorage,
});
Batch Updates
import { batch } from '@legendapp/state';
import { appState$ } from '../store/appStore';
// Batch multiple changes into a single notification
function loginUser(name: string, email: string) {
batch(() => {
appState$.user.name.set(name);
appState$.user.email.set(email);
appState$.user.isLoggedIn.set(true);
});
// Components re-render only once after the batch completes
}
Arrays and Collection Patterns
import { appState$ } from '../store/appStore';
// Add an item
function addTodo(text: string) {
appState$.todos.push({
id: crypto.randomUUID(),
text,
completed: false,
});
}
// Update a specific item by index
function toggleTodo(index: number) {
appState$.todos[index].completed.toggle();
}
// Remove an item — filter and reassign
function removeTodo(id: string) {
appState$.todos.set(
appState$.todos.get().filter((t) => t.id !== id)
);
}
Reactive Components with Memo
import { Memo } from '@legendapp/state/react';
import { appState$ } from '../store/appStore';
// Memo renders only the observable's value, isolating re-renders
// to just the text node — the parent component never re-renders
function Header() {
return (
<h1>
Welcome, <Memo>{appState$.user.name}</Memo>
</h1>
);
}
Best Practices
- Use
observer()on all components that read observables — without it,.get()calls will read the value but the component will not re-render on changes. - Prefer fine-grained access — read
state$.user.name.get()rather thanstate$.user.get().nameso the component only subscribes to thenamefield. - Use
batch()for multi-field updates — batching prevents intermediate re-renders when setting several values at once.
Common Pitfalls
- Forgetting
observer()wrapper — components will read stale values and never re-render. Every component that calls.get()must be wrapped withobserver()or use the<Memo>component. - Reading too broadly with
.get()on a parent object — callingstate$.user.get()subscribes to every field inuser. Access the specific leaf node instead to keep re-renders minimal.
Anti-Patterns
Over-engineering for hypothetical requirements. Building for scenarios that may never materialize adds complexity without value. Solve the problem in front of you first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide wastes time and introduces risk.
Premature abstraction. Creating elaborate frameworks before having enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at system boundaries. Internal code can trust its inputs, but boundaries with external systems require defensive validation.
Skipping documentation. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add state-management-skills
Related Skills
Jotai Atomic State Management
Jotai atomic state for React — primitive/derived/async atoms, Provider-less mode, atomWithStorage, atomWithQuery, and React Suspense integration
Nanostores
Nanostores tiny framework-agnostic state manager — atoms, computed stores, lifecycle events, and integrations with React, Vue, Svelte, and vanilla JS
Redux Toolkit
Redux Toolkit for scalable React state — createSlice, configureStore, RTK Query, createAsyncThunk, entity adapter, middleware, and TypeScript patterns
TanStack Query (React Query)
TanStack Query for server state — useQuery, useMutation, query invalidation, optimistic updates, infinite queries, prefetching, and SSR hydration
Valtio Proxy State Management
Valtio proxy-based state for React — mutable-style API with automatic tracking, snapshots, derived state, and nested object support
XState State Machines
XState for state machines and statecharts in React — actors, guards, actions, services, @xstate/react integration, and the visual editor