Fine Grained Reactivity
To get the best performance with React it's ideal to make components as small as possible so that state changes re-render the minimum number of components. The methods and components described on this page help you make sure that each change causes only the smallest and fewest renders.
See React Examples for real-world examples of using these components.
Render an observable directly
Render an observable string
or number
straight into React and it will automatically be extracted as a separate memoized component with its own tracking context. This does not need the component to be an observer
.
You just need to call enableLegendStateReact()
once at the beginning of your application to enable it.
import { enableLegendStateReact } from "@legendapp/state/react"
enableLegendStateReact()
const count = observable(0)
function Optimized() {
// This never re-renders when observable is rendered directly
return (
<div>Count: {count}</div>
)
}
See enableLegendStateReact.ts if you're curious how it works.
Computed
Computed extracts children so that their changes do not affect the parent, but the parent's changes will still re-render them. Use this when children use observables that change often without affecting the parent, but also depends on local state in the parent.
The child needs to be a function to be able to extract it into a separate tracking context, but the Babel plugin lets you pass it children directly.
// With Babel plugin
<Computed>
{state.messages.map(message => (
<div key={message.id}>{message.text} {localVar}</div>
)}
</Computed>
// Without Babel plugin
<Computed>
{() => state.messages.map(message => (
<div key={message.id}>{message.text} {localVar}</div>
)}
</Computed>
Example
In this example see that clicking the "Render parent" button renders the parent and increments value
and the computed children are updated too.
import { Computed } from "@legendapp/state/react"
function ComputedExample() {
const renderCount = ++useRef(0).current
const state = useObservable({ count: 0 })
const [value, setValue] = useState(1)
const onClick = () => setValue(v => v + 1)
useInterval(() => {
state.count.set(c => c + 1)
}, 500)
return (
<div>
<div>Renders: {renderCount}</div>
<div>Value: {value}</div>
<Computed>
<div>Value: {value}</div>
<div>Count: {state.count}</div>
</Computed>
</div>
)
})
Memo
Memo is similar to Computed, but it will never re-render from parent changes - only if its own observables change. Use Memo
when children are truly independent from the parent component. This is equivalent to extracting it as a separate function.
The child needs to be a function to be able to extract it into a separate tracking context, but the Babel plugin lets you pass it children directly.
// With Babel plugin
<Memo>
{state.messages.map(message => (
<div key={message.id}>{message.text}</div>
)}
</Memo>
// Without Babel plugin
<Memo>
{() => state.messages.map(message => (
<div key={message.id}>{message.text}</div>
)}
</Memo>
Example
This is the same as the Computed example, except that the memoized children are not updated with the parent's value.
import { Memo } from "@legendapp/state/react"
function MemoExample() {
const renderCount = ++useRef(0).current
const state = useObservable({ count: 0 })
const [value, setValue] = useState(1)
const onClick = () => setValue(v => v + 1)
useInterval(() => {
state.count.set(c => c + 1)
}, 500)
return (
<div>
<div>Renders: {renderCount}</div>
<div>Value: {value}</div>
<Memo>
<div>Value: {value}</div>
<div>Count: {state.count}</div>
</Memo>
</div>
)
})
Show
Show renders child components conditionally based on the if/else props, and does not re-render the parent when the condition changes.
Passing children as a function can prevent the JSX from being created until it needs to render. That's done automatically if you use the babel plugin.
Props:
if
: A computed function or an observableelse
: Optionally provide a component to render if the condition is not metchildren
: The components to show conditionally. This can be React elements or a function given the value returned fromif
which you can use to do more complex conditional rendering.
<Show
if={state.show}
else={<div>Nothing to see here</div>}
>
<Modal />
</Show>
Example
import { Show } from "@legendapp/state/react"
function ShowExample() {
const renderCount = ++useRef(0).current
const state = useObservable({ show: false })
const onClick = () => state.show.set(show => !show)
return (
<div>
<div>Renders: {renderCount}</div>
<button onClick={onClick}>Toggle</button>
<!-- 1. Direct children -->
<Show
if={state.show}
else={<div>Nothing to see here</div>}
>
<Modal />
</Show>
<!-- 2. With a function -->
<Show
if={() => state.show.get()}
else={() => <div>Nothing to see here</div>}
>
{() => <div>Modal</div>}
</Show>
</div>
)
})
Switch
Switch renders one child component conditionally based on the value
prop, and does not re-render the parent when the condition changes.
Props:
value
: A computed function or an observablechildren
: An object with the possible cases ofvalue
as keys. Ifvalue
doesn't match any of the cases it will use thedefault
case if available.
<Switch value={state.index)}>
{{
0: () => <div>Tab 1</div>,
1: () => <div>Tab 2</div>,
default: () => <div>Error</div>
}}
</Switch>
Example
import { Switch } from "@legendapp/state/react"
function SwitchExample() {
const renderCount = ++useRef(0).current
const tabIndex = useObservable(0)
const onClick = () => tabIndex.set(v => v > 2 ? 0 : v + 1)
return (
<div>
<div>Renders: {renderCount}</div>
<button onClick={onClick}>Next tab</button>
<Switch value={tabIndex}>
{{
0: () => <Tab1 />,
1: () => <Tab2 />,
2: () => <Tab3 />,
default: () => <Error />
}}
</Switch>
</div>
)
})
Optionally add the Babel plugin
The Babel plugin can make the syntax for Computed and Memo less verbose. But they work fine without Babel if you don't want to or can't use it. The Babel plugin converts the JSX under the hood so you don't need to use functions as children. It basically does this:
// You write
<Computed><div>Count: {state.count.get()}</div></Computed>
// Babel transforms it to
<Computed>{() => <div>Count: {state.count.get()}</div>}</Computed>
Install
Add @legendapp/state/babel
to the plugins in your babel.config.js
:
module.exports = {
plugins: [
...
"@legendapp/state/babel",
],
}
If you're using typescript you can add a .d.ts
file to your project with this in it, to expand the types to allow direct children to Computed and Memo.
/// <reference types="@legendapp/state/types" />