Legend-State provides reactive components that enable ultra-fine-grained rendering. These components can update the DOM directly without re-rendering their parent component, making your app incredibly fast.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/LegendApp/legend-state/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Reactive components accept observables as children or props and update only the specific parts of the UI that need to change:import { useObservable } from '@legendapp/state/react'
import { Memo } from '@legendapp/state/react'
function App() {
const count$ = useObservable(0)
// App never re-renders, only the Memo updates
return (
<div>
Count: <Memo>{count$}</Memo>
<button onClick={() => count$.set(v => v + 1)}>+</button>
</div>
)
}
Memo Component
TheMemo component renders an observable’s value and updates only itself when the observable changes.
Signature
function Memo(props: {
children: ObservableParam | (() => ReactNode)
scoped?: boolean
}): ReactElement
Basic Usage
- With Observable
- With Function
- Scoped
import { observable } from '@legendapp/state'
import { Memo } from '@legendapp/state/react'
const count$ = observable(0)
function Counter() {
// Counter never re-renders when count changes
return (
<div>
Count: <Memo>{count$}</Memo>
<button onClick={() => count$.set(v => v + 1)}>
Increment
</button>
</div>
)
}
import { useObservable } from '@legendapp/state/react'
import { Memo } from '@legendapp/state/react'
function Component() {
const user$ = useObservable({ firstName: 'John', lastName: 'Doe' })
return (
<div>
Name: <Memo>{() =>
`${user$.firstName.get()} ${user$.lastName.get()}`
}</Memo>
</div>
)
}
import { useObservable } from '@legendapp/state/react'
import { Memo } from '@legendapp/state/react'
function Component() {
const count$ = useObservable(0)
return (
<div>
{/* With scoped, Memo re-creates when function reference changes */}
<Memo scoped>
{() => <ExpensiveComponent count={count$.get()} />}
</Memo>
</div>
)
}
Performance Benefits
import { useObservable } from '@legendapp/state/react'
import { Memo } from '@legendapp/state/react'
let parentRenders = 0
function ParentComponent() {
parentRenders++
const count$ = useObservable(0)
return (
<div>
<div>Parent rendered {parentRenders} times</div>
<div>Count: <Memo>{count$}</Memo></div>
<button onClick={() => count$.set(v => v + 1)}>+</button>
</div>
)
}
// ParentComponent only renders once!
// Clicking the button updates only the Memo
Show Component
Conditionally renders children based on a predicate, with support for else clauses and ready state.Signature
function Show<T>(props: {
if?: Selector<T>
ifReady?: Selector<T>
else?: ReactNode | (() => ReactNode)
$value?: Observable<T>
wrap?: FC<{ children: ReactNode }>
children: ReactNode | ((value?: T) => ReactNode)
}): ReactElement
Basic Usage
- Simple Condition
- With Else
- With Value
- If Ready
import { useObservable } from '@legendapp/state/react'
import { Show } from '@legendapp/state/react'
function Component() {
const isLoggedIn$ = useObservable(false)
return (
<div>
<Show if={isLoggedIn$}>
<div>Welcome back!</div>
</Show>
<button onClick={() => isLoggedIn$.set(true)}>
Log In
</button>
</div>
)
}
import { observable } from '@legendapp/state'
import { Show } from '@legendapp/state/react'
const user$ = observable(null)
function UserProfile() {
return (
<Show
if={() => user$.get() !== null}
else={<div>Please log in</div>}
>
<div>Welcome, {user$.name.get()}</div>
</Show>
)
}
import { observable } from '@legendapp/state'
import { Show } from '@legendapp/state/react'
const user$ = observable({ name: 'John', age: 30 })
function Component() {
return (
<Show if={() => user$.age.get() >= 18} $value={user$}>
{(user) => (
<div>{user.name} is an adult</div>
)}
</Show>
)
}
import { useObservable } from '@legendapp/state/react'
import { Show } from '@legendapp/state/react'
function AsyncComponent() {
const data$ = useObservable(async () => {
const res = await fetch('/api/data')
return res.json()
})
return (
<Show
ifReady={data$}
else={<div>Loading...</div>}
>
{(data) => <div>Data: {data.name}</div>}
</Show>
)
}
For Component
Renders lists efficiently, automatically keying items and updating only changed items.Signature
function For<T, TProps>(props: {
each?: ObservableParam<T[] | Record<any, T> | Map<any, T>>
optimized?: boolean
item?: FC<ForItemProps<T, TProps>>
itemProps?: TProps
sortValues?: (A: T, B: T, AKey: string, BKey: string) => number
children?: (value: Observable<T>, id: string | undefined) => ReactElement
}): ReactElement | null
type ForItemProps<T, TProps = {}> = {
item$: Observable<T>
id?: string
} & TProps
Basic Usage
- Array
- With Children Function
- Object/Map
- With ItemProps
- Sorted
import { observable } from '@legendapp/state'
import { For } from '@legendapp/state/react'
import { observer } from '@legendapp/state/react'
const todos$ = observable([
{ id: '1', text: 'Learn Legend-State', done: false },
{ id: '2', text: 'Build an app', done: false }
])
const TodoItem = observer(function TodoItem({ item$ }) {
return (
<div>
<input
type="checkbox"
checked={item$.done.get()}
onChange={(e) => item$.done.set(e.target.checked)}
/>
{item$.text.get()}
</div>
)
})
function TodoList() {
return (
<div>
<For each={todos$} item={TodoItem} />
</div>
)
}
import { observable } from '@legendapp/state'
import { For } from '@legendapp/state/react'
const items$ = observable(['Apple', 'Banana', 'Cherry'])
function List() {
return (
<div>
<For each={items$}>
{(item$, id) => (
<div key={id}>
{item$.get()}
</div>
)}
</For>
</div>
)
}
import { observable } from '@legendapp/state'
import { For } from '@legendapp/state/react'
const users$ = observable({
user1: { name: 'John', age: 30 },
user2: { name: 'Jane', age: 25 }
})
function UserList() {
return (
<For each={users$}>
{(user$, id) => (
<div key={id}>
{id}: {user$.name.get()} ({user$.age.get()})
</div>
)}
</For>
)
}
import { observable } from '@legendapp/state'
import { For } from '@legendapp/state/react'
import { observer } from '@legendapp/state/react'
const items$ = observable([1, 2, 3])
const Item = observer(function Item({ item$, prefix }) {
return <div>{prefix}: {item$.get()}</div>
})
function List() {
return <For each={items$} item={Item} itemProps={{ prefix: 'Item' }} />
}
import { observable } from '@legendapp/state'
import { For } from '@legendapp/state/react'
const items$ = observable([
{ name: 'Banana', price: 2 },
{ name: 'Apple', price: 1 },
{ name: 'Cherry', price: 3 }
])
function SortedList() {
return (
<For
each={items$}
sortValues={(a, b) => a.price - b.price}
>
{(item$) => (
<div>{item$.name.get()} - ${item$.price.get()}</div>
)}
</For>
)
}
Optimized Mode
import { observable } from '@legendapp/state'
import { For } from '@legendapp/state/react'
const items$ = observable([1, 2, 3, 4, 5])
function OptimizedList() {
return (
<For each={items$} optimized>
{(item$) => <div>{item$.get()}</div>}
</For>
)
}
optimized mode uses shallow tracking to minimize re-renders when only array length changes.Switch Component
Renders different content based on a value, like a switch statement.Signature
function Switch<T>(props: {
value?: Selector<T>
children: Partial<Record<any, () => ReactNode>>
}): ReactElement | null
Usage
- Basic
- With States
- Boolean
import { useObservable } from '@legendapp/state/react'
import { Switch } from '@legendapp/state/react'
function StatusIndicator() {
const status$ = useObservable('loading')
return (
<Switch value={status$}>
{{
loading: () => <div>Loading...</div>,
success: () => <div>Success!</div>,
error: () => <div>Error occurred</div>,
default: () => <div>Unknown status</div>
}}
</Switch>
)
}
import { observable } from '@legendapp/state'
import { Switch } from '@legendapp/state/react'
const view$ = observable('list')
function App() {
return (
<div>
<nav>
<button onClick={() => view$.set('list')}>List</button>
<button onClick={() => view$.set('grid')}>Grid</button>
<button onClick={() => view$.set('table')}>Table</button>
</nav>
<Switch value={view$}>
{{
list: () => <ListView />,
grid: () => <GridView />,
table: () => <TableView />,
default: () => <div>Select a view</div>
}}
</Switch>
</div>
)
}
import { useObservable } from '@legendapp/state/react'
import { Switch } from '@legendapp/state/react'
function ToggleView() {
const isEnabled$ = useObservable(true)
return (
<Switch value={isEnabled$}>
{{
true: () => <div>Enabled</div>,
false: () => <div>Disabled</div>
}}
</Switch>
)
}
Reactive Object
TheReactive object provides reactive versions of all HTML elements that accept observable props.
Usage
import { useObservable } from '@legendapp/state/react'
import { Reactive } from '@legendapp/state/react'
function Component() {
const text$ = useObservable('Hello World')
const color$ = useObservable('blue')
const isVisible$ = useObservable(true)
return (
<div>
{/* Props prefixed with $ are reactive */}
<Reactive.div
$style={() => ({ color: color$.get() })}
$className={() => isVisible$.get() ? 'visible' : 'hidden'}
>
<Reactive.span $children={text$} />
</Reactive.div>
<input
value={text$.get()}
onChange={(e) => text$.set(e.target.value)}
/>
</div>
)
}
All HTML Elements
Reactive provides reactive versions of all HTML elements:
import { Reactive } from '@legendapp/state/react'
<Reactive.div $children={...} />
<Reactive.span $children={...} />
<Reactive.button $onClick={...} />
<Reactive.input $value={...} />
<Reactive.img $src={...} />
// ... and all other HTML elements
reactive() HOC
Create reactive versions of your own components:function reactive<T>(component: FC<T>): FC<ShapeWith$<T>>
function reactive<T, K>(
component: FC<T>,
keys: K[],
bindKeys?: BindKeys<T, K>
): FC<ReactifyProps<T, K>>
- Basic
- With Keys
- With Binding
import { reactive } from '@legendapp/state/react'
import { useObservable } from '@legendapp/state/react'
function Card({ title, subtitle }) {
return (
<div>
<h3>{title}</h3>
<p>{subtitle}</p>
</div>
)
}
const ReactiveCard = reactive(Card)
function App() {
const title$ = useObservable('Hello')
const subtitle$ = useObservable('World')
return <ReactiveCard $title={title$} $subtitle={subtitle$} />
}
import { reactive } from '@legendapp/state/react'
function Input({ value, onChange }) {
return <input value={value} onChange={onChange} />
}
// Only 'value' prop is reactive
const ReactiveInput = reactive(Input, ['value'])
function App() {
const text$ = useObservable('')
return (
<ReactiveInput
$value={text$}
onChange={(e) => text$.set(e.target.value)}
/>
)
}
import { reactive } from '@legendapp/state/react'
function Input({ value, onChange }) {
return <input value={value} onChange={onChange} />
}
const ReactiveInput = reactive(
Input,
['value'],
{
value: {
handler: 'onChange',
getValue: (e) => e.target.value
}
}
)
function App() {
const text$ = useObservable('')
// Two-way binding!
return <ReactiveInput $value={text$} />
}
Best Practices
- Use Memo for simple values - When you just need to display an observable value
- Use Show for conditions - Better than ternary operators for reactive conditions
- Use For for lists - More efficient than
.map()for observable arrays - Use Switch for multiple states - Cleaner than nested conditions
- Use Reactive for granular updates - Update specific props without re-rendering
Reactive components are most beneficial when:
- Parent component is expensive to render
- Updates are frequent
- Only small parts of UI need to update
observer might be simpler.See Also
observer() HOC
Automatic tracking for components
Fine-Grained Rendering
Advanced performance patterns
React Hooks
Complete hooks reference