Skip to content

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.

Read more

Example

This example shows:

  1. State persisted to local storage
  2. Reactive components