Skip to main content
observer is a higher-order component (HOC) that wraps a React component to automatically track observable access and re-render when tracked observables change.

Usage

import { observer } from '@legendapp/state/react'
import { state$ } from './state'

const Counter = observer(function Counter() {
  // Component automatically re-renders when state$.count changes
  return (
    <div>
      <div>Count: {state$.count.get()}</div>
      <button onClick={() => state$.count.set(v => v + 1)}>
        Increment
      </button>
    </div>
  )
})

Signature

function observer<P extends FC<any>>(component: P): P

Parameters

component
FC<P>
required
A React functional component to wrap with automatic observable tracking.The component can be:
  • A regular function component
  • A component wrapped with forwardRef
  • A component wrapped with memo
The observer will unwrap and re-wrap these HOCs automatically.

Returns

component
FC<P>
The wrapped component with automatic observable tracking. The component is automatically wrapped with React.memo for performance optimization.

Examples

Basic usage

const TodoList = observer(() => {
  const todos = state$.todos.get()
  
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  )
})

With props

interface Props {
  userId: string
}

const UserProfile = observer(({ userId }: Props) => {
  const user = state$.users[userId].get()
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  )
})

Accessing nested observables

const ProductDetails = observer(() => {
  // Each .get() is tracked - component re-renders when any of these change
  const name = state$.product.name.get()
  const price = state$.product.price.get()
  const inStock = state$.product.inStock.get()
  
  return (
    <div>
      <h2>{name}</h2>
      <p>Price: ${price}</p>
      {inStock ? <span>In Stock</span> : <span>Out of Stock</span>}
    </div>
  )
})

With forwardRef

const Input = observer(
  forwardRef<HTMLInputElement, { label: string }>((props, ref) => {
    const value = state$.inputValue.get()
    
    return (
      <div>
        <label>{props.label}</label>
        <input
          ref={ref}
          value={value}
          onChange={(e) => state$.inputValue.set(e.target.value)}
        />
      </div>
    )
  })
)

Conditional tracking

const ConditionalDisplay = observer(() => {
  const showDetails = state$.showDetails.get()
  
  return (
    <div>
      <h1>Header</h1>
      {showDetails && (
        // state$.details is only tracked when showDetails is true
        <div>{state$.details.get()}</div>
      )}
    </div>
  )
})

Multiple observables

const Dashboard = observer(() => {
  const user = state$.currentUser.get()
  const notifications = state$.notifications.get()
  const messages = state$.messages.get()
  
  return (
    <div>
      <Header user={user} />
      <NotificationBadge count={notifications.length} />
      <MessageList messages={messages} />
    </div>
  )
})

With derived data

const CartSummary = observer(() => {
  const items = state$.cart.items.get()
  
  // Computed inline - recalculates on every render but only re-renders when items change
  const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  const itemCount = items.reduce((sum, item) => sum + item.quantity, 0)
  
  return (
    <div>
      <p>Items: {itemCount}</p>
      <p>Total: ${total.toFixed(2)}</p>
    </div>
  )
})

Nested observer components

const TodoItem = observer(({ id }: { id: string }) => {
  // Only re-renders when this specific todo changes
  const todo = state$.todos[id].get()
  
  return (
    <div>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={(e) => state$.todos[id].completed.set(e.target.checked)}
      />
      <span>{todo.text}</span>
    </div>
  )
})

const TodoList = observer(() => {
  // Only re-renders when the todo list length/order changes
  const todoIds = state$.todos.get().map(t => t.id)
  
  return (
    <div>
      {todoIds.map(id => <TodoItem key={id} id={id} />)}
    </div>
  )
})

Behavior

Automatic tracking

observer wraps the entire component render in a tracking context. Any observable accessed with .get() during render is automatically tracked:
const Component = observer(() => {
  // ✅ Tracked - will cause re-render
  const value = state$.value.get()
  
  // ❌ Not tracked - callback runs after render
  const handleClick = () => {
    const other = state$.other.get()
  }
  
  return <div>{value}</div>
})

Fine-grained reactivity

Only the observables actually accessed during render are tracked:
const Component = observer(() => {
  const user = state$.user.get() // Tracks state$.user
  
  // Only tracks state$.user.name, not other user properties
  return <div>{user.name}</div>
})

Memoization

Observer components are automatically wrapped with React.memo, preventing unnecessary re-renders from parent components:
const Child = observer(({ id }: { id: string }) => {
  const item = state$.items[id].get()
  return <div>{item.name}</div>
})

// Child only re-renders when state$.items[id] changes,
// NOT when Parent re-renders
function Parent() {
  return <Child id="123" />
}

Optimization with useSelector

When observer detects a useSelector call with a plain observable (not a function), it optimizes away the extra subscription:
const Component = observer(() => {
  // Optimized - no extra subscription created
  const count = useSelector(state$.count)
  
  return <div>{count}</div>
})

Performance considerations

Component granularity

Break large components into smaller observer components for optimal re-rendering:
// ❌ Less optimal - entire component re-renders for any state change
const Dashboard = observer(() => {
  return (
    <div>
      <div>{state$.user.name.get()}</div>
      <div>{state$.count.get()}</div>
      <div>{state$.messages.get().length} messages</div>
    </div>
  )
})

// ✅ Better - each section only re-renders when its data changes
const UserSection = observer(() => <div>{state$.user.name.get()}</div>)
const CountSection = observer(() => <div>{state$.count.get()}</div>)
const MessageSection = observer(() => <div>{state$.messages.get().length} messages</div>)

function Dashboard() {
  return (
    <div>
      <UserSection />
      <CountSection />
      <MessageSection />
    </div>
  )
}

Avoid object/array creation

Creating new objects/arrays on every render bypasses React.memo optimization:
// ❌ Child re-renders even when data doesn't change
const Parent = observer(() => {
  return <Child data={{ value: state$.value.get() }} />
})

// ✅ Pass primitives or observables directly
const Parent = observer(() => {
  return <Child value={state$.value.get()} />
})

// ✅ Or pass the observable itself
const Parent = observer(() => {
  return <Child value$={state$.value} />
})

When to use observer vs useSelector

Use observer when:

  • Component accesses multiple observables
  • Observable access is spread throughout the component
  • You want automatic tracking without manual subscriptions
  • You want the simplest, most concise code

Use useSelector when:

  • Component only needs one or two observable values
  • The component is already complex and you want explicit control
  • You need fine-grained control over what triggers re-renders
// observer - simple and automatic
const Component = observer(() => {
  return <div>{state$.a.get()} {state$.b.get()} {state$.c.get()}</div>
})

// useSelector - more explicit
function Component() {
  const a = useSelector(state$.a)
  const b = useSelector(state$.b)
  const c = useSelector(state$.c)
  return <div>{a} {b} {c}</div>
}

Type Parameters

P
type
The props type of the component. Must extend FC<any> (functional component).

Notes

  • Observer components are automatically memoized
  • Works with forwardRef and memo - automatically unwraps and re-wraps
  • Only tracks observables accessed during render, not in callbacks
  • In development mode, provides helpful warnings about render loops
  • Marked as already proxied to prevent double-wrapping in Fast Refresh
  • reactive - Make components accept observable props
  • useSelector - Manual observable subscription
  • useObserve - Side effects on observable changes

Build docs developers (and LLMs) love