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>
    )
})
Renders: 1
Value: 1
Value: 1
Count: 0

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>
    )
})
Renders: 1
Value: 1
Value: 1
Count: 0

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 observable
  • else: Optionally provide a component to render if the condition is not met
  • children: The components to show conditionally. This can be React elements or a function given the value returned from if 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>
    )
})
Renders: 1
Nothing to see here

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 observable
  • children: An object with the possible cases of value as keys. If value doesn't match any of the cases it will use the default 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>
    )
})
Renders: 0
Tab 1

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" />