React Introduction
This is a high level overview for how to work with Legend-State in React. See React API for more specific details. Legend-State supports both React and React Native, but most of the examples are in React for ease of showing live demos.
Component reactivity
The first step to working with observables in React is to make your components re-render when observable values change. Legend-State has three ways for your components to update themselves based on observables. This applies to all observables, whether in global state or local within a component.
enableReactTracking
To get started we recommend enableReactTracking - it’s the easiest way to work with observables in React. You only need to enable this once in your app’s entry point.
import { observable } from "@legendapp/state";
import { enableReactTracking } from "@legendapp/state/config/enableReactTracking";
enableReactTracking({
auto: true,
});
const name$ = observable("Annyong");
function Component() {
// The component re-renders when name changes
const name = name$.get();
return <div>{name}</div>;
}
We recommend using this to get started and for rapid prototyping, but it’s always better to use observer
. Under the hood, when called from a React component, it replaces get()
with a useSelector
hook. So keep in mind that it acts like a hook, so it is subject to the rules of hooks. So it can cause errors when used conditionally or within loops.
observer
observer turns the component into an observing context so that .get()
will trigger re-render when observable change. This is the best and most efficient way to use observables in React. Just wrap your components in observer
to make them efficiently track all accessed observables.
import { observable } from "@legendapp/state";
import { observer } from "@legendapp/state/react";
const state$ = observable({ count: 0 });
const Component = observer(function Component() {
// Accessing state$ automatically makes this component track changes to re-render
const count = state$.count.get();
// Re-renders whenever count changes
return <div>{count}</div>;
});
useSelector
useSelector
returns the value of an observable or a function and updates the component when its value changes. You can use it for more complex cases to compute a value based on observables, and only re-render when its return value changes. See useSelector for more in-depth details.
import { observable } from "@legendapp/state";
const state$ = observable({ fname: "hello", lname: "there" });
function Component() {
// Re-render when fname changes
const fname = useSelector(state$.fname);
// Re-render when the computed value of fullname changes
const fullname = useSelector(
() => `${state$.fname.get()} ${state$.lname.get()}`
);
return (
<div>
{fname} {fullname}
</div>
);
}
Local state
In addition to using global state, you can create local state with useObservable
to use immediately or pass down through children or context.
import { observer, useObservable } from "@legendapp/state/react";
function App() {
const store$ = useObservable({
profile: { name: "hi" },
});
// This component does not get() the store so only Profile will re-render on changes
return (
<div>
<Profile profile$={store$.profile} />
</div>
);
}
const Profile = observer(function Profile({ profile$ }) {
const name = profile$.name.get();
return <div>Name: {name}</div>;
})
Fine-grained reactivity
A new (and fun) pattern with Legend-State is to make re-renders fine-grained so that your full components don’t re-render at all - focusing updates to our tiny fine-grained components.
The most basic way to optimize renders is to have observable text or numbers render themselves, so their parent component doesn’t have to.
You can take it even further with the Reactive
components and control-flow components like For
, Show
, and Switch
. Combining these we can use what we call a “render once” style - components render only the first time and state changes trigger only the tiniest possible re-renders. Especially with very large components, rendering the full component less often can give you a huge performance boost.
import { observable } from "@legendapp/state";
import { Memo, For, Reactive, Show, Switch } from "@legendapp/state/react";
const state$ = observable({ showModal: false, page: 0, users: [] });
function MemoExample() {
// This component itself never re-renders
return (
<div>
// Reactive components have reactive props and children which re-render
themselves on changes
<Reactive.div
$className={state$.showModal.get() ? "bg-blue-500" : "bg-red-500"}
>
{() => `Showing page: ${state$.page.get()}`}
</Reactive.div>
// Show re-renders itself whenever showModal changes
<Show if={state$.showModal}>{() => <Modal />}</Show>
// Switch re-renders itself whenever page changes
<Switch value={state$.page}>
{{
0: <Page0 />,
1: <Page1 />,
2: <Page2 />,
}}
</Switch>
// For optimizes array updates to be much faster
<For each={state$.users} item={User} optimized />
</div>
);
}
function User({ item }) {
return <Memo>{item.name}</Memo>;
}
These fine-grained reactivity patterns are inspired by Knockout.js as well as more modern frameworks like Solid and Svelte. They are a great way to optimize your apps, but teams who want a more canonical React experience or easy migration may want to ignore or use them sparingly to optimize specific heavy components.
Example
This example shows:
- State persisted to local storage
- Reactive components