Fine Grained Reactivity
Legend-State enables a new way of thinking about how React components update: to observe state changing rather than observing renders. In this pattern, components render once and individual elements re-render themselves. This enables what we call a “render once” style - components render only the first time and state changes trigger only the tiniest possible re-renders.
You can render observable primitives directly in mini self-updating components, use reactive props to update props based on state, or use a set of control-flow components to optimize conditional rendering and arrays to re-render as little as possible.
Some teams may prefer to use Legend-State in a way that’s more canonically React and skip some or all of these concepts, at least at first. But the fine-grained reactivity features can improve performance and reduce the amount of code you need to write. See Making React fast by default and truly reactive for more details.
Render an observable/selector directly
Use the Memo
component to create a mini element that re-renders itself when it changes, without needing the parent component to re-render. This is the most basic and recomended way for using Legend-State with React. The children inside of Memo
re-render themselves when the value changes, but the parent component does not re-render.
Reactive components
Legend-State provides reactive versions of all platform components with reactive props. This lets you provide a Selector to props so that the component will update itself whenever the Selector changes.
For input elements it can create a two-way binding to the value, so that the observable is always in sync with the displayed value of the element.
Control-flow components
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.
This is equivalent to extracting it as a separate component (and passing in all needed props).
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.
In this example see that clicking the “Render parent” button renders the parent and increments value
and the computed children are updated too.
import { useInterval } from "usehooks-ts" import { useRef, useState } from "react" import { observable } from "@legendapp/state" import { Computed, observer, useObservable } from "@legendapp/state/react" const ComputedExample = () => { const renderCount = ++useRef(0).current const [value, setValue] = useState(1) // Only the Computed component tracks this const state$ = useObservable({ count: 1 }) useInterval(() => { state$.count.set((v) => v + 1) }, 500) // Force a render const onClick = () => setValue((v) => v + 1) return ( <Box center> <h5>Normal</h5> <div>Renders: {renderCount}</div> <div>Value: {value}</div> <Button onClick={onClick}> Render </Button> <Computed> {() => <> <h5>Computed</h5> <div>Value: {value}</div> <div>Count: {state$.count.get()}</div> </>} </Computed> </Box> ) }
Memo
Memo is similar to Computed, but it will never re-render when the parent component renders - 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 component (and passing in all needed props) with React.memo
.
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.
This is the same as the Computed example, except that the memoized children are not updated with the parent’s value.
import { useInterval } from "usehooks-ts" import { observable } from "@legendapp/state" import { useRef, useState } from "react" import { Memo, observer, useObservable } from "@legendapp/state/react" const MemoExample = () => { const renderCount = ++useRef(0).current const [value, setValue] = useState(1) // Only the Memo'd component tracks this const state$ = useObservable({ count: 1 }) useInterval(() => { state$.count.set((v) => v + 1) }, 500) // Force a render const onClick = () => setValue((v) => v + 1) return ( <Box center> <h5>Normal</h5> <div>Renders: {renderCount}</div> <div>Value: {value}</div> <Button onClick={onClick}> Render </Button> <Memo> {() => <> <h5>Memo'd</h5> <div>Value: {value}</div> <div>Count: {state$.count.get()}</div> </>} </Memo> </Box> ) }
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 observableifReady
: A computed function or an observable. This will not render if the value is an empty object or empty array.else
: 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.wrap
: A component to wrap the children. For example this could be Framer Motion’s AnimatePresence to animate the element entering/exiting.
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.
For
The For
component is optimized for rendering arrays of observable objects so that they are extracted into a separate tracking context and don’t re-render the parent.
An optimized
prop adds additional optimizations, but in an unusual way by re-using React nodes. See Optimized rendering for more details.
Props:
each
: An observable (array, object, or Map)item
: A render function which receives the item id, and item observable or undefineditemProps
: Extra props to pass down to each itemsortValues
: If theeach
parameter is an object or Map, this is a sort function for how to sort the elements.(A: T, B: T, AKey: string, BKey: string) => number
children
: A render function or, you can pass a render function as children instead of in theitem
prop if you prefer.
Optionally add the Babel plugin
The Babel plugin can make the syntax for Computed, Memo, and Show 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 converts inline elements to functions so that they can be treated reactively:
To install it, add @legendapp/state/babel
to the plugins in your babel.config.js
:
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 JSX children to Computed and Memo.
Create your own reactive components
reactive
You can wrap external components in reactive
to create reactive versions of all of their props, prefixed with $
. This makes it so that the reactive component can accept reactive props but the target receives regular props as usual. reactive
creates a Proxy (not an HOC) that extracts all reactive props and observes them for changes, passing the regular prop down to the component.
In this example, reactive
adds a $message
prop which takes a Selector, while the target component receives a normal message
prop and is only re-rendered when message
changes.
In addition to wrapping your own functions, you can wrap external library components to make them reactive. In this example we make a Framer Motion component reactive so that we can update its animations based on observables without needing to re-render the parent component or its children.
reactiveObserver
This is a single HOC with the functionality of both observer
and reactive
. They both run the same function under the hood, with slightly different options, so this is the optimal way to have one HOC that does both at once.
reactiveComponents
reactiveComponents
makes multiple reactive components at once. You can use this to create your own internal library of reactive components, or to wrap UI libraries that have multiple components in a namespace like Modal.Header
and Modal.Footer
.