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.

1. use()

We recommend adding the use() function to your observables - it is the easiest way to work with observables in React as you will see in the examples. You only need to do this once in your app's entry point.

import { enableReactUse } from '@legendapp/state/config/enableReactUse'
import { observable } from "@legendapp/state"

enableReactUse() // This adds the use() function to observables

const name$ = observable('Annyong')

function Component() {
    // The component re-renders when name changes
    const name = name$.use()

    return <div>{name}</div>
}

2. 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, or if you prefer it to .use() or . 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>
}

3. observer HOC

The observer HOC turns the component into an observing context so that any .get() calls will be observed to trigger re-render when they change. That can be useful for reusing existing functions that use .get(). It also optimizes by grouping all use() calls together instead of creating multiple hooks. In practice that's a micro-optimization that's only really useful in huge components.

This is a more advanced feature so we suggest that you start with use() and useSelector, and return to observer as you're getting more comfortable with Legend-State.

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>
})

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 { observable } from "@legendapp/state"
import { useObservable } from "@legendapp/state/react"

function App() {
    const store$ = useObservable({
        profile: { name: 'hi' }
    })

    // This component does not use() the store so only Profile will re-render on changes

    return (
        <div>
            <Profile profile$={store$.profile} />
        </div>
    )
}

function Profile({ profile$ }) {
    const name = profile$.name.use()

    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.

import { Memo, useObservable } from "@legendapp/state/react"

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>
            // Memo re-renders itself when count changes
            <div>Count: <Memo>{state$.count}</Memo></div>
        </div>
    )
}
Renders: 1
Count: 0

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.

Read more

Example

This example shows:

  1. State persisted to local storage
  2. Reactive components
import { useRef } from "react";
import { reactive, Reactive } from "@legendapp/state/react";
import { enableReactComponents } from "@legendapp/state/config/enableReactComponents";
import { motion } from "framer-motion";
import { State } from "./State";
enableReactComponents();

const MotionDiv = reactive(motion.div);

export default function App() {
  const renderCount = ++useRef(0).current;

  return (
    <div className="flex absolute inset-0">
      <MotionDiv
        className="bg-gray-600 text-center pt-2 text-white text-sm"
        $initial={() => ({
            width: State.settings.showSidebar.get() ? 96 : 0
        })}
        $animate={() => ({
            width: State.settings.showSidebar.get() ? 96 : 0
        })}
      >
        Sidebar
      </MotionDiv>
      <div className="flex-1 p-4">
        <div className="text-gray-500 text-sm pb-4">Renders: {renderCount}</div>
        <div>Username:</div>
        <Reactive.input
          className={classNameInput}
          $value={State.user.profile.name}
        />
        <div>
          <button
            className="bg-gray-300 rounded-lg px-4 py-2 mt-6"
            onClick={State.settings.showSidebar.toggle}
          >
            Toggle sidebar
          </button>
        </div>
        <div>
          <button
            className="bg-gray-300 rounded-lg px-4 py-2 mt-6"
            onClick={() => window.location.reload()}
          >
            Refresh
          </button>
        </div>
      </div>
    </div>
  );
}

const classNameInput = "border rounded border-gray-300 px-2 py-1 mt-2";