Why?

Legend-State is an evolution of the state system we've been using internally in Legend since 2015 and in Bravely since 2020. It needs to be extremely fast because Legend users have documents with hundreds of thousands of items. We recently rewrote it with modern browser features, optimizing for both developer experience and best possible performance / memory usage. Comparing to other state libraries, we think you'll prefer Legend-State for these reasons:

Tiny and FAST ⚡️

Legend-State is the fastest React state library, designed to be as efficient as possible. It does very little extra work and minimizes renders by only re-rendering components when their observables change. And at only 3kb it won't hurt your bundle size.

Feels natural 😌

There are no special hooks, functions, Contexts, or other boilerplate required to work with observables. Observable objects work as you'd expect, and the observable functions are right there on the prototype.

const obs = observable({ value: 1 })
obs.value.get()
obs.value.set(2)

// Components automatically track observables and re-render when they change
// No HOC or selector needed
function Component {
    return <div>Value: {obs.value}</div>
}

Fine-grained reactivity

Rendering an observable automatically extracts it as a separate memoized component with its own tracking context under the hood, so it's easy to isolate renders to a single text element.

const count = observable(0)

function Normal() {
    // This re-renders when count changes
    return (
        <div>Count: {count.get()}</div>
    )
}
function FineGrained() {
    // This never re-renders when observable is rendered directly
    return (
        <div>Count: {count}</div>
    )
}
Normal
Renders: 1
Count: 0
Fine-grained
Renders: 1
Count:
0

For isolating a group of elements or computations, Legend-State has built-in helpers to easily extract children so that their changes do not affect the parent. This keeps large parent components from rendering often just because their children change.

function MemoExample() {
    const renderCount = ++useRef(0).current
    const messages = useObservable([])

    useInterval(() => {
        messages.splice(0, 0,
            `Message ${messages.length + 1}`)
    }, 1000)

    return (
        <div>
            <div>Renders: {renderCount}</div>
            <Memo>
                <div>
                    {messages.map((m, i) => (
                        <div key={i}>{m}</div>
                    ))}
                </div>
            </Memo>
        </div>
    )
})
Renders: 1

Unopinionated 🤷‍♀️

Some state libraries are for global state while some want state to reside within React. Some enourage individual atoms and others are for large global stores. Some have "actions" and "reducers" and others require immutability. But you can use Legend-State any way you want.

  • Global state or local state in React: Up to you 🤷‍♀️
  • Individual atoms or one store: Up to you 🤷‍♀️
  • Modify directly or in actions/reducers: Up to you 🤷‍♀️

See Patterns for more examples of different ways to use Legend-State.

Persistence built in 💾

There are only two hard things in Computer Science: cache invalidation and naming things. - Phil Karlton

We don't want developers to have to worry about persisting and syncing state, because it's often very complicated and error-prone. So we've built persistence plugins using Legend-State's listeners, with extensive tests to make sure it's absolutely correct.

It currently includes plugins for local persistence with Local Storage on web and react-native-mmkv in React Native, with more persistence plugins coming soon, including persisting remotely to Firebase.

const state = observable({ settings: { theme: 'dark' } })

persistObservable(state, { local: 'exampleState' })

It's safe from 🔫 footguns

Observables prevent direct assignment, favoring more purposeful set and assign functions instead. Read more in safety.