Skip to main content
useObserve is a React hook that runs a callback function whenever tracked observables change. It’s similar to useEffect but automatically tracks observable dependencies.

Usage

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

function Component() {
  // Runs whenever state$.count changes
  useObserve(() => {
    console.log('Count changed:', state$.count.get())
  })
  
  return <div>Check console</div>
}

Signature

// Single callback form
function useObserve<T>(
  run: (e: ObserveEvent<T>) => T | void,
  options?: UseObserveOptions
): () => void

// Selector + reaction form
function useObserve<T>(
  selector: Selector<T>,
  reaction?: (e: ObserveEventCallback<T>) => any,
  options?: UseObserveOptions
): () => void

Parameters

Single callback form

run
(e: ObserveEvent<T>) => T | void
required
A function that runs when any tracked observables change. The function automatically tracks any observables accessed with .get().The callback receives an ObserveEvent object with:
  • previous: The previous value
  • value: The current value (if tracking a specific observable)
  • num: The number of times this observer has fired

Selector + reaction form

selector
Selector<T>
required
A function or observable to track. Changes to this selector trigger the reaction.
reaction
(e: ObserveEventCallback<T>) => any
A function that runs when the selector changes. Receives the same ObserveEvent object.If omitted, the selector function itself is treated as both the tracker and the side effect.

Options

options
UseObserveOptions
Configuration options.

Returns

dispose
() => void
A cleanup function that stops observing. Called automatically when the component unmounts.

Examples

Basic side effect

useObserve(() => {
  const count = state$.count.get()
  document.title = `Count: ${count}`
})

With selector and reaction

// Only re-runs when user.name changes
useObserve(
  () => state$.user.name.get(),
  ({ value, previous }) => {
    console.log(`Name changed from ${previous} to ${value}`)
  }
)

Multiple observables

useObserve(() => {
  const firstName = state$.user.firstName.get()
  const lastName = state$.user.lastName.get()
  
  analytics.track('Name viewed', {
    fullName: `${firstName} ${lastName}`
  })
})

With dependencies

function UserLogger({ userId }) {
  // Recreate observer when userId prop changes
  useObserve(
    () => {
      console.log('User data:', state$.users[userId].get())
    },
    { deps: [userId] }
  )
}

// Alternative: pass deps as second parameter
useObserve(
  () => {
    console.log('User data:', state$.users[userId].get())
  },
  [userId]
)

Run immediately

useObserve(
  () => {
    console.log('Count:', state$.count.get())
  },
  { immediate: true }
)
// Logs immediately on mount, then on every change

Async effects

useObserve(() => {
  const userId = state$.currentUserId.get()
  
  if (userId) {
    fetchUserData(userId).then(data => {
      state$.userData.set(data)
    })
  }
})

Manual cleanup

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

// Later: stop observing
dispose()

Access event metadata

useObserve((e) => {
  console.log('Previous:', e.previous)
  console.log('Current:', e.value)
  console.log('Call count:', e.num)
  
  const count = state$.count.get()
  console.log('Count:', count)
})

Behavior

Automatic tracking

useObserve automatically tracks any observables accessed with .get() during execution:
useObserve(() => {
  if (state$.isLoggedIn.get()) {
    // This is only tracked when isLoggedIn is true
    const userName = state$.user.name.get()
    console.log('Logged in as:', userName)
  }
})

Selector vs single callback

Using a separate selector and reaction can be more efficient:
// ❌ Less efficient - entire function re-runs
useObserve(() => {
  const count = state$.count.get()
  expensiveOperation(count)
})

// ✅ More efficient - expensive operation only in reaction
useObserve(
  () => state$.count.get(),
  ({ value }) => {
    expensiveOperation(value)
  }
)

Lifecycle

  • Observer is set up on component mount
  • Runs when tracked observables change
  • Automatically disposed on component unmount
  • Recreated if deps change

Comparison with useEffect

// useEffect - manual dependencies
useEffect(() => {
  console.log('Count:', state$.count.get())
}, [state$.count.get()]) // ❌ Needs manual tracking

// useObserve - automatic tracking
useObserve(() => {
  console.log('Count:', state$.count.get())
}) // ✅ Automatically tracks state$.count

Type Parameters

T
type
The type of the tracked value. Inferred from the selector or callback.

Notes

  • Does not cause component re-renders (use useSelector for that)
  • Callbacks are not memoized - they use the latest closure values
  • The selector function re-runs on each change to re-track dependencies
  • Use deps when the observer needs to respond to prop/state changes

Build docs developers (and LLMs) love