Skip to main content
Legend-State provides a comprehensive set of React hooks for managing observable state, observing changes, and integrating with React’s lifecycle.

State Hooks

useObservable

Creates a new observable that persists for the lifetime of the component.
function useObservable<T>(): Observable<T | undefined>
function useObservable<T>(initialValue: T): Observable<T>
function useObservable<T>(initialValue: () => T): Observable<T>
function useObservable<T>(initialValue: Promise<T>): Observable<T>
function useObservable<T>(initialValue: T, deps?: DependencyList): Observable<T>
import { useObservable } from '@legendapp/state/react'

function Counter() {
    const count$ = useObservable(0)

    return (
        <div>
            <div>Count: {count$.get()}</div>
            <button onClick={() => count$.set(v => v + 1)}>
                Increment
            </button>
        </div>
    )
}
Alias: useLocalObservable is an alias for useObservable

useComputed

Creates a computed observable that automatically updates when dependencies change.
function useComputed<T>(get: () => T): Observable<T>
function useComputed<T>(get: () => T, deps: any[]): Observable<T>
function useComputed<T, T2>(get: () => T, set: (value: T2) => void): Observable<T>
import { useObservable, useComputed } from '@legendapp/state/react'

function FullName() {
    const user$ = useObservable({ 
        firstName: 'John', 
        lastName: 'Doe' 
    })

    const fullName$ = useComputed(() => 
        `${user$.firstName.get()} ${user$.lastName.get()}`
    )

    return (
        <div>
            <div>Full Name: {fullName$.get()}</div>
            <input 
                value={user$.firstName.get()}
                onChange={e => user$.firstName.set(e.target.value)}
            />
        </div>
    )
}
For simple computed values, you can also use observable(() => ...) directly:
const fullName$ = observable(() => 
    `${user$.firstName.get()} ${user$.lastName.get()}`
)

useObservableReducer

Creates an observable with reducer pattern, similar to useReducer.
function useObservableReducer<R extends Reducer<any, any>>(
    reducer: R,
    initialState: ReducerState<R>
): [Observable<ReducerState<R>>, Dispatch<ReducerAction<R>>]
import { useObservableReducer } from '@legendapp/state/react'

function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 }
        case 'decrement':
            return { count: state.count - 1 }
        default:
            return state
    }
}

function Counter() {
    const [state$, dispatch] = useObservableReducer(reducer, { count: 0 })

    return (
        <div>
            <div>Count: {state$.count.get()}</div>
            <button onClick={() => dispatch({ type: 'increment' })}>
                +
            </button>
            <button onClick={() => dispatch({ type: 'decrement' })}>
                -
            </button>
        </div>
    )
}

Selection Hooks

useSelector

Renders a component when the selected value changes. This is the core hook for fine-grained reactivity.
function useSelector<T>(selector: Selector<T>, options?: UseSelectorOptions): T

interface UseSelectorOptions {
    suspense?: boolean
    skipCheck?: boolean
}
import { observable } from '@legendapp/state'
import { useSelector } from '@legendapp/state/react'

const count$ = observable(0)

function Counter() {
    const count = useSelector(count$)

    return <div>Count: {count}</div>
}
Aliases: use$ and useValue are aliases for useSelector

Observation Hooks

useObserve

Runs a callback when tracked observables change. Returns a dispose function.
function useObserve<T>(callback: (e: ObserveEvent<T>) => void): () => void
function useObserve<T>(
    selector: Selector<T>,
    reaction: (e: ObserveEventCallback<T>) => void,
    options?: UseObserveOptions
): () => void
import { useObservable, useObserve } from '@legendapp/state/react'

function Component() {
    const count$ = useObservable(0)

    useObserve(() => {
        console.log('Count changed:', count$.get())
    })

    return (
        <button onClick={() => count$.set(v => v + 1)}>
            Increment
        </button>
    )
}

useObserveEffect

Like useObserve, but only runs once on mount (similar to useEffect).
function useObserveEffect<T>(callback: (e: ObserveEvent<T>) => void): void
function useObserveEffect<T>(
    selector: Selector<T>,
    reaction: (e: ObserveEventCallback<T>) => void,
    options?: UseObserveOptions
): void
import { observable } from '@legendapp/state'
import { useObserveEffect } from '@legendapp/state/react'

const user$ = observable({ name: 'John' })

function Component() {
    useObserveEffect(() => {
        // This logs on mount and whenever user.name changes
        console.log('User name:', user$.name.get())
    })

    return <div>{user$.name.get()}</div>
}

Lifecycle Hooks

useMount

Runs a callback once when component mounts. Can return a cleanup function.
function useMount(fn: () => void | (() => void)): void
import { useMount } from '@legendapp/state/react'

function Component() {
    useMount(() => {
        console.log('Component mounted')

        return () => {
            console.log('Component unmounting')
        }
    })

    return <div>Hello</div>
}

useUnmount

Runs a callback when component unmounts.
function useUnmount(fn: () => void): void
import { useUnmount } from '@legendapp/state/react'

function Component() {
    useUnmount(() => {
        console.log('Component unmounted')
    })

    return <div>Hello</div>
}

useIsMounted

Returns an observable boolean that tracks mount state.
function useIsMounted(): Observable<boolean>
import { useIsMounted } from '@legendapp/state/react'

function Component() {
    const isMounted$ = useIsMounted()

    const handleAsync = async () => {
        const data = await fetchData()
        // Only update if still mounted
        if (isMounted$.get()) {
            updateState(data)
        }
    }

    return <button onClick={handleAsync}>Load Data</button>
}

Utility Hooks

useWhen

Returns a promise that resolves when a predicate becomes truthy.
function useWhen<T>(predicate: Selector<T>): Promise<T>
function useWhen<T, T2>(
    predicate: Selector<T>,
    effect: (value: T) => T2
): Promise<T2>
import { observable } from '@legendapp/state'
import { useWhen } from '@legendapp/state/react'

const user$ = observable({ name: undefined })

function Component() {
    useMount(async () => {
        // Wait until name is set
        const name = await useWhen(() => user$.name.get())
        console.log('Name is ready:', name)
    })

    return <div>{user$.name.get()}</div>
}

useWhenReady

Like useWhen, but resolves when value is not undefined.
function useWhenReady<T>(predicate: Selector<T>): Promise<T>
import { useObservable, useWhenReady } from '@legendapp/state/react'

function Component() {
    const data$ = useObservable(async () => {
        const res = await fetch('/api/data')
        return res.json()
    })

    useMount(async () => {
        const data = await useWhenReady(data$)
        console.log('Data loaded:', data)
    })

    return <div>Loading...</div>
}

Best Practices

  1. Use useObservable for local state - Create observables that live within components
  2. Use useSelector for fine-grained updates - Re-render only when specific values change
  3. Use useObserve for side effects - React to changes without triggering re-renders
  4. Clean up with useUnmount - Dispose of subscriptions and timers properly
Don’t call .get() in render without tracking:
// Bad - won't re-render on changes
const value = count$.peek()

// Good - tracks and re-renders
const value = useSelector(count$)

See Also

observer() HOC

Wrap components for automatic tracking

Reactive Components

Components that accept observable props

Fine-Grained Rendering

Advanced performance patterns

Build docs developers (and LLMs) love