Introduction

Legend-State is a super fast and powerful state library for JavaScript apps with three primary goals:

1. 🔥 Fine-grained reactivity for minimal renders

Legend-State lets you make your renders super fine-grained, making your apps much faster because React has to do less work. The best way to be fast is to render less, less often.

function Normal() {
    const [count, setCount] = useState(0);
    // This re-renders when count changes
    return (
        <div>Count: {count}</div>
    )
}
function FineGrained() {
    const count = useObservable(0)
    // The text updates itself so the component doesn't re-render
    return (
        <div>Count: {count}</div>
    )
}
Normal
Renders: 1
Count: 1
Fine-grained
Renders: 1
Count:
1

2. 🦄 As easy as possible to use

There is no boilerplate and there are no contexts, actions, reducers, selectors, dispatchers, sagas, thunks, or epics. Just call get() to get the raw data and set() to change it.

In React there are no selectors, hooks, or higher order components. Just access observables and your components update themselves automatically.

// Create an observable object
const state = observable({ settings: { theme: 'dark' } })

// Just get and set
state.settings.theme.get() === 'dark'
state.settings.theme.set('light')

// observe re-runs when accessed observables change
observe(() => {
    console.log(state.settings.theme.get())
})

// Observer components automatically track observables and re-render when they change
const Component = observer(function Component() {
    const theme = state.settings.theme.get()

    return <div>Theme: {theme}</div>
})

3. ⚡️ The fastest React state library

Legend-State beats every other state library on just about every metric and is so optimized for arrays that it even beats vanilla JS on the "swap" and "replace all rows" benchmarks. At only 3kb and with the massive reduction in boilerplate code, you'll have big savings in file size too.

See Fast 🔥 for more details of why Legend-State is so fast.

Example

import { observable } from "@legendapp/state"

// Create an observable object
const state = observable({ settings: { theme: 'dark' } })

// get() returns the raw data
state.settings.theme.get() === 'dark'

// observe re-runs when any observables change
observe(() => {
    console.log(state.settings.theme.get())
})

// Assign to state with set
state.settings.theme.set('light')

// Automatically persist state. Refresh this page to try it.
persistObservable(state, { local: 'exampleState' })

// Components re-render only when accessed observables change
// This is the code for the example on your right ----->
const Component = observer(function Component() {
    const theme = state.settings.theme.get()
    // state.settings.theme is automatically tracked for changes

    const toggle = () => {
        state.settings.theme.set(theme =>
            theme === 'dark' ? 'light' : 'dark'
        )
    }

    return (
        <div
            className={theme === 'dark' ? 'theme-dark' : 'theme-light'}
        >
            <div>Theme: {theme}</div>
            <Button onClick={toggle}>
                Toggle theme
            </Button>
        </div>
    )
}
Theme: light

Highlights

  • ✨ Super easy to use 😌
  • ✨ Super fast ⚡️
  • ✨ Super small at 3kb 🐥
  • ✨ Fine-grained reactivity 🔥
  • ✨ No boilerplate
  • ✨ Designed for maximum performance and scalability
  • ✨ React components re-render only on changes
  • ✨ Very strongly typed with TypeScript
  • ✨ Persistence plugins for automatically saving/loading from storage
  • ✨ State can be global or within components

The core is platform agnostic so you can use it in vanilla JS or any framework to create and listen to observables. It includes support for React and React Native, and has plugins for automatically persisting to storage.

Read more about why you'll love Legend-State ❤️

Install

npm
yarn
npm i @legendapp/state

Core usage

You can put anything in an observable: primitives, deeply nested objects, arrays, functions, etc... Observables track changes on all nested objects and notify listeners whenever anything changes. Observables work just like normal objects so you can interact with them without any extra complication. Just call get() to get the value and set(...) to modify it.

import { observable } from "@legendapp/state"

const obs = observable({ text: 'hello', obj: { value: 10 } })

obs.text.get() === 'hello' // true
obs.obj.value.get() === 10 // true

// Use the set function anywhere
obs.text.set('hi')

// Easily modify the previous value
obs.text.set(text => text + ' there')

Read more

Observe changes

observable makes every value anywhere within the state object observable, so you can listen to changes anywhere within the object tree. You can listen for changes on a specific observable or use observe to automatically track any observables accessed.

const obs = observable({ settings: { theme: 'light' }, array: [{text: 'hi'}] })

// Listen to observable directly
obs.settings.theme.onChange(({ value }) => console.log('Theme is', value))

// Or observe
observe(() => {
    console.log('Theme is', obs.settings.theme)
})

Use when to wait for a value to become truthy.

await when(() => obs.settings.theme === 'dark')

Read more

React usage

Legend-State's React integration automatically listens to the accessed observables for changes while rendering. Components will re-render only when these observables change, so it's ideal to be as specific as possible to minimize renders. See Performance for more optimization tips.

import { enableLegendStateReact, observer } from "@legendapp/state/react"

// Enable direct rendering of observables
enableLegendStateReact()

const obs = observable({ text: 'hello', num: 10, other: {} as LargeObject })

// Wrap component in observer to make it automatically track observable changes
const Component = observer(function Component() {
    // This component will never re-render.
    // The two text elements will re-render themselves on changes to text or num
    return <div>{obs.text} {obs.num}</div>
})

Read more

Easy fine-grained reactivity

Use the Computed and Memo components to isolate children so that they re-render from their own observables without needing to re-render the parent. This is a very easy way to optimize large components to render less often.

Or just render an observable directly to give it its own tracking context.

function MemoExample() {
    const renderCount = ++useRef(0).current
    const state = useObservable({ count: 0 })

    useInterval(() => {
        state.count.set(v => v + 1)
    }, 500)

    return (
        <div>
            <div>Renders: {renderCount}</div>
            // Render observable directly
            <div>Count: {state.count}</div>
            // Memo creates separate tracking context
            <Memo>
                <div>Count: {state.count.get()}</div>
            </Memo>
        </div>
    )
})
Renders: 1
Count: 0
Count: 0

Read more

Get the raw data

You may want to access the underlying data to modify without notifying, or to check for strict equality. You can just call get() on any observable to get the raw value.

const obs = observable({ profile: { name: '' } })
const profile = { name: 'Test user' }
obs.profile.set(profile)

obs.profile === profile       // ❌ false. The observable is a Proxy.
obs.profile.get() === profile // ✅ true. The raw data is exactly what was set.

Read more

Computed values

computed automatically tracks the observables accessed while computing, so you can just return a computed value based on multiple observables, and it will update whenever one of them changes.

computed is lazy so it won't run the compute function until you get() the value the first time.

const obs = observable({ first: 'Hi', last: 'there' })
const computed = computed(() => obs.first.get() + ' ' + obs.last.get())
// computed.get() === 'Hi there'

obs.first.set('Hello')
// computed.get() === 'Hello there'

Read more

Observable primitives

observable is also optimized to be able to use many observable primitives instead of one big object, if you prefer.

const theme = observable('dark')

// Get the value with get()
theme.get() === 'dark' // true

// Set the value with set()
theme.set('light')

Read more

Persistence plugins

Use persistObservable to automatically persist state using any kind of local or remote storage. Legend-State includes local providers for Local Storage on web and react-native-mmkv in React Native, with more local and remote providers coming soon. Use configureObservablePersistence to set default providers for all persisted observables, or you can set them individually if they need to be different.

The given observables will be populated with their persisted state immediately after calling persistObservable.

// Global configuration
configureObservablePersistence({
    // Use Local Storage on web
    persistLocal: ObservablePersistLocalStorage
    // Use react-native-mmkv in React Native
    persistLocal: ObservablePersistMMKV
})

const obs = observable({ store: { bigObject: { ... } } })

// Persist this observable
persistObservable(obs, {
    local: 'store' // Unique name
})

Read more

Batch changes

You may want to modify multiple observables at once without triggering renders for each change. Batching functions delay renders and listeners until the end of the batch.

import { batch, beginBatch, endBatch } from '@legendapp/state'
const obs = observable({ items: [] })

function addItems() {
    for (let i = 0; i < 1000; i ++) {
        obs.items.push({ text: `Item ${i}` })
    }
}

// Wrap in begin and end
beginBatch()
addItems()
endBatch()

// Or batch with a callback
batch(() => {
    addItems()
})

Read more

Create actions

If you prefer to modify your stores with actions, you can do that by adding functions to the observables, although it's not required.

const obs = observable({
    settings: {
        theme: 'light'
    },
    setTheme: (theme) => obs.settings.theme.set(theme)
})

Or you can have external management functions for modifying state if you prefer.

export function setTheme(theme) {
    obs.settings.theme.set(theme)
}

Read more