Skip to main content

Overview

Legend-State provides three primary methods for reading observable values and several ways to update them. Understanding when to use each method is key to building reactive and performant applications.

Reading Values

get() - Reactive Access

The get() method reads the current value and automatically tracks the access for reactivity:
const count$ = observable(0)

// Reactive read - will cause re-renders/re-runs when count changes
const value = count$.get()
console.log(value) // 0

// In a reactive context
observe(() => {
  console.log('Count is:', count$.get())
})

count$.set(1) // Logs: "Count is: 1"

Shallow Tracking

You can limit tracking to only shallow changes:
const state$ = observable({
  user: {
    name: 'Alice',
    age: 30
  }
})

observe(() => {
  // Only tracks changes to the user object itself, not nested properties
  const user = state$.user.get(true) // Pass true for shallow
  console.log('User changed:', user)
})

state$.user.name.set('Bob')  // Does not trigger
state$.user.set({ name: 'Charlie', age: 35 }) // Triggers

peek() - Non-Reactive Access

The peek() method reads the current value without tracking it:
const count$ = observable(0)
const name$ = observable('Alice')

observe(() => {
  console.log('Count:', count$.get())    // Tracked
  console.log('Name:', name$.peek())     // Not tracked
})

count$.set(1) // Triggers the observer
name$.set('Bob') // Does NOT trigger the observer
Use peek() when you need to read a value inside a reactive context but don’t want to create a dependency on it.

Direct Property Access

For nested observables, you can chain property access:
const state$ = observable({
  user: {
    profile: {
      name: 'Alice',
      email: '[email protected]'
    }
  }
})

// All of these create observables
const user$ = state$.user
const profile$ = state$.user.profile
const name$ = state$.user.profile.name

// Then read with get() or peek()
name$.get() // 'Alice'

Type Signatures

interface Observable<T> {
  get(trackingType?: TrackingType | GetOptions): T
  peek(): T
}

type TrackingType = undefined | true | symbol
// true === shallow tracking
// undefined === deep tracking (default)

interface GetOptions {
  shallow?: boolean
}

Setting Values

set() - Update Values

The set() method updates the observable’s value:
const count$ = observable(0)

// Set to a new value
count$.set(5)
console.log(count$.get()) // 5

// Set nested properties
const user$ = observable({
  name: 'Alice',
  age: 30
})

user$.name.set('Bob')
user$.age.set(31)

set() with Functions

Pass a function to compute the new value based on the previous value:
const count$ = observable(0)

// Increment
count$.set(prev => prev + 1)

// Conditional update
const todos$ = observable([{ text: 'Buy milk', done: false }])
todos$[0].done.set(prev => !prev)
The function receives the raw value, not an observable. You don’t need to call .get() on it.

Setting Objects

When setting an object, it replaces the entire value:
const user$ = observable({
  name: 'Alice',
  age: 30,
  email: '[email protected]'
})

// This REPLACES the entire object
user$.set({
  name: 'Bob',
  age: 25,
  email: '[email protected]'
})

// email is gone because we didn't include it
user$.set({
  name: 'Charlie',
  age: 35
}) // email property is now deleted

Setting Arrays

Arrays can be set directly or modified with array methods:
const items$ = observable([1, 2, 3])

// Replace entire array
items$.set([4, 5, 6])

// Array methods work reactively
items$.push(7)
items$.pop()
items$.splice(1, 1, 10) // Remove index 1, insert 10

// Set individual elements
items$[0].set(100)

Setting with Observables

You can set an observable to link to another observable:
const source$ = observable({ value: 10 })
const target$ = observable({ data: null })

// Link target to source
target$.data.set(source$)

// Now they're linked
console.log(target$.data.value.get()) // 10
source$.value.set(20)
console.log(target$.data.value.get()) // 20

Advanced Setting

assign() - Merge Objects

Use assign() to merge properties instead of replacing:
const user$ = observable({
  name: 'Alice',
  age: 30,
  email: '[email protected]'
})

// Only updates specified properties
user$.assign({
  age: 31
})

// name and email are preserved
console.log(user$.get())
// { name: 'Alice', age: 31, email: '[email protected]' }

delete() - Remove Values

Delete an observable or a property:
const data$ = observable({
  a: 1,
  b: 2,
  c: 3
})

// Delete a property
data$.b.delete()
console.log(data$.get()) // { a: 1, c: 3 }

// Delete from array
const items$ = observable([1, 2, 3, 4])
items$[1].delete() // Removes element at index 1
console.log(items$.get()) // [1, 3, 4]

toggle() - Boolean Values

Boolean observables have a convenient toggle() method:
const isOpen$ = observable(false)

isOpen$.toggle() // Sets to true
isOpen$.toggle() // Sets to false

// Equivalent to:
isOpen$.set(prev => !prev)

Type Signatures

interface Observable<T> {
  set(value: T): void
  set(value: (prev: T) => T): void
  set(value: Promise<T>): void
  set(value: Observable<T>): void
  
  assign(value: Partial<T>): Observable<T>
  delete(): void
  toggle(): void // Only on Observable<boolean>
}

Batching Updates

When making multiple changes, batch them for better performance:
import { batch } from '@legendapp/state'

const state$ = observable({
  count: 0,
  name: 'Alice',
  items: []
})

// All changes notify together
batch(() => {
  state$.count.set(10)
  state$.name.set('Bob')
  state$.items.set([1, 2, 3])
})
// Only one notification fires after the batch
See the Batching page for more details.

Common Patterns

Increment/Decrement

const count$ = observable(0)

// Increment
count$.set(n => n + 1)

// Decrement
count$.set(n => n - 1)

// Add value
count$.set(n => n + 5)

Toggle State

const flags$ = observable({
  isOpen: false,
  isActive: true,
  isLoading: false
})

flags$.isOpen.toggle()
flags$.isActive.set(prev => !prev) // Alternative

Conditional Updates

const user$ = observable({
  name: 'Alice',
  age: 30
})

user$.age.set(age => age >= 18 ? age + 1 : age)

Array Operations

const todos$ = observable([
  { id: 1, text: 'Buy milk', done: false },
  { id: 2, text: 'Walk dog', done: true }
])

// Add item
todos$.push({ id: 3, text: 'Write docs', done: false })

// Remove item
todos$.splice(1, 1)

// Update item
todos$[0].done.set(true)

// Toggle item
todos$[0].done.toggle()

Best Practices

Use peek() for Side Effects

When reading values in side effects that shouldn’t trigger re-runs, use peek() instead of get().

Batch Multiple Updates

Wrap multiple set() calls in batch() to avoid unnecessary re-renders.

Prefer assign() for Partials

Use assign() to update some properties without affecting others.

Use Functions for Computations

When the new value depends on the old value, always use a function with set().

Next Steps

Observing Changes

Learn how to react to value changes

Batching

Optimize updates with batching

Build docs developers (and LLMs) love