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>
)
}
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>
)
}
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 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')
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')
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>
})
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>
)
})
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.
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'
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')
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
})
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()
})
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)
}