React Examples

The examples on this page use Tailwind CSS for styling and Framer Motion for animations. These examples all use the fine-grained-reactivity components so that the parent component renders only once and all renders are optimized to be as small as possible.

Persisted global state

This example creates a global state object and persists it to Local Storage. Try changing the username and toggling the sidebar and refreshing - it will restore it to the previous state.

import { observable } from '@legendapp/state'
import { persistObservable } from '@legendapp/state/persist'
import { ObservablePersistLocalStorage } from '@legendapp/state/persist-plugins/local-storage'

export const State = observable({
  settings: {
    showSidebar: false
  },
  user: {
    profile: {
      name: ''
    }
  }
})

persistObservable(State, {
  local: 'example',
  persistLocal: ObservablePersistLocalStorage,
})

Auto-saving Form

This example uses the useObservableQuery hook to create an observable using TanStack Query that automatically sends mutations back to the server whenever the observable changes.

It then uses the Legend two-way binding components to bind those observable directly to the inputs.

So in effect this binds the inputs directly to your server data.

import axios from 'axios'
import { useRef } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { enableLegendStateReact, useObservable } from '@legendapp/state/react'
import { Legend } from '@legendapp/state/react-components'
import { useObservableQuery } from '@legendapp/state/react-hooks/useObservableQuery'
import { debounce } from './debounce'

enableLegendStateReact()

const queryClient = new QueryClient()

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

function Example() {
  const renderCount = ++useRef(0).current
  const { data } = useObservableQuery(
    {
      queryKey: ["data"],
      queryFn: () =>
        axios.get("https://reqres.in/api/users/1")
          .then((res) => res.data.data),
    },
    {
      mutationFn: (newData) => {
        // Uncomment to actually save
        /*
        debounce(() => {
          axios
            .post("https://reqres.in/api/users/1", newData)
            .then((res) =>
              lastSaved.set(Date.now())
            )
        }, 1000)
        */
        lastSaved.set(Date.now())
      }
    }
  )
  const lastSaved = useObservable(0)

  return (
    <div className="p-4">
      <div className="text-gray-500 text-sm pb-4">
        Renders: {renderCount}
      </div>
      <div>Name:</div>
      <Legend.input
        className={classNameInput}
        value$={data.first_name}
      />
      <div>Email:</div>
      <Legend.input
        className={classNameInput}
        value$={data.email}
      />
      <div>
        Last saved: {lastSaved}
      </div>
    </div>
  )
}

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

Form validating

This example uses useObserve to listen to changes in the form state to update the error messages as you type. It waits for the first click of the Save button for a better user experience.

import { useRef } from 'react'
import { enableLegendStateReact, useObservable, useObserve } from '@legendapp/state/react'
import { Legend } from '@legendapp/state/react-components'

enableLegendStateReact()

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

  const username = useObservable('')
  const password = useObservable('')
  const usernameError = useObservable('')
  const passwordError = useObservable('')
  const didSave = useObservable(false)

  useObserve(() => {
    if (didSave.get()) {
      usernameError.set(username.get().length < 3 ?
        'Username must be > 3 characters' :
        ''
      )

      const pass = password.get()
      passwordError.set(
        pass.length < 10 ?
          'Password must be > 10 characters' :
          !pass.match(/^(?=.*d)$/) ?
            'Password must include a number' :
            ''
      )
    }
  })

  const onClickSave = () => {
    // changing didSave runs useObserve immediately, updating error messages
    didSave.set(true)

    if (!usernameError.get() && !passwordError.get()) {
      console.log('Submit form')
    }
  }

  return (
    <div className="p-4">
      <div className="text-gray-500 text-sm pb-4">
        Renders: {renderCount}
      </div>
      <div>Username:</div>
      <Legend.input
        className={classNameInput}
        value$={username}
      />
      <div className={classNameError}>
        {usernameError}
      </div>
      <div>Password:</div>
      <Legend.input
        type="password"
        className={classNameInput}
        value$={password}
      />
      <div className={classNameError}>
        {passwordError}
      </div>
      <div>
        <button
          className="bg-gray-300 rounded-lg px-4 py-2 mt-6"
          onClick={onClickSave}
        >
          Save
        </button>
      </div>
    </div>
  )
}

const classNameInput = "border rounded border-gray-300 px-2 py-1 mt-2"
const classNameError = "text-sm text-red-500 mb-2 h-5 pt-1"

List of messages

This example uses the useFetch hook to get data from a server as an observable, useComputed to create a computed observable, and For to display the array of messages in a high-performance way.

import { useRef } from 'react'
import { enableLegendStateReact, useComputed, useObservable, useObserve, For, Show } from '@legendapp/state/react'
import { useFetch } from '@legendapp/state/react-hooks/useFetch'
import { Legend } from '@legendapp/state/react-components'

enableLegendStateReact()

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

  // Create profile from fetch promise
  const {
    data: { data: profile },
  } = useFetch('https://reqres.in/api/users/1')

  // Username
  const userName = useComputed(() => {
    const p = profile.get()
    return p ?
        p.first_name + ' ' + p.last_name :
        ''
  })

  // Chat state
  const { messages, currentMessage } = useObservable({
    messages: [],
    currentMessage: ''
  })

  // Button click
  const onClickAdd = () => {
    messages.push({
      id: generateID(),
      text: currentMessage.get(),
    })
    currentMessage.set('')
  }

  return (
    <div className="p-4">
      <div className="text-gray-500 text-sm pb-4">
        Renders: {renderCount}
      </div>
      <Show if={userName} else={<div>Loading...</div>}>
        <div>Chatting with {userName}</div>
      </Show>
      <div className="h-64 p-2 my-3 overflow-auto border border-gray-300 rounded">
        <For each={messages}>{(message) => <div>{message.text}</div>}</For>
      </div>
      <div className="flex gap-2">
        <Legend.input
          className="flex-1 px-2 border border-gray-300 rounded min-w-0"
          placeholder="Enter message"
          value$={currentMessage}
          onKeyDown={e => e.key === 'Enter' && onClickAdd()}
        />
        <button
          className="bg-gray-300 rounded-lg px-4 py-2"
          onClick={onClickAdd}
        >
          Send
        </button>
      </div>
    </div>
  )
}

let nextID = 0
function generateID() {
  return nextID ++
}

Animations with reactive props

This example uses reactive to make a version of motion.div with reactive props that can animate using observable values. Animating with reactive props is faster than re-rendering the whole component because when the tracked observable changes it triggers a render of only the motion.div, so it doesn't need to re-render the parent or children.

This example also creates a computed observable text value from the boolean and renders it directly in JSX, which (under the hood) creates a reactive text element that re-renders itself when it changes.

import { reactive } from '@legendapp/state/react'
import { motion } from "framer-motion"

const MotionDiv$ = reactive(motion.div)

export function Toggle({ value }) {
  return (
    <MotionDiv$
      className="border border-gray-200 rounded-full select-none"
      animate$={() => ({
        backgroundColor: value.get() ? '#6ACB6C' : '#C4D1E3'
      })}
      style={{ width: 64, height: 32 }}
      onClick={value.toggle}
    >
      <MotionDiv$
        className="bg-white rounded-full shadow"
        style={{ width: 24, height: 24, marginTop: 3 }}
        animate$={() => ({
          x: value.get() ? 32 : 6
        })}
      />
    </MotionDiv$>
  )
}

Show a modal with multiple pages

This example uses Show to show/hide a modal based on an observable value, and Switch to render the active page in the modal.

import { observable } from '@legendapp/state'
import { Show, useComputed, useObservable } from '@legendapp/state/react'
import { Legend } from '@legendapp/state/react-components'
import { AnimatePresence } from "framer-motion"
import { useRef } from 'react'
import { Modal } from './Modal'

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

  const showModal = useObservable(false)

  return (
    <div className="absolute inset-0 p-4">
      <div className="text-gray-500 text-sm pb-4">
        Renders: {renderCount}
      </div>
      <button
        className="bg-gray-300 rounded-lg px-4 py-2"
        onClick={showModal.toggle}
      >
        Show modal
      </button>
      <Show if={showModal} wrap={AnimatePresence}>
        {() => <Modal show={showModal} />}
      </Show>
    </div>
  )
}

Router

function RouterExample() {
  const renderCount = ++useRef(0).current;

  return (
    <div style={{ width: 300 }}>
      <div>Renders: {renderCount}</div>
      <div>
        <button
          onClick={() => pageHashParams.page.delete()}
        >
          Go to root
        </button>
        <button
          onClick={() => pageHashParams.page.set('')}
        >
          Go to Page
        </button>
        <button
          onClick={() => pageHashParams.page.set('Home')}
        >
          Go Home
        </button>
        <button
          onClick={() => pageHashParams.page.set('asdf')}
        >
          Go to unknown
        </button>
      </div>
      <Switch value={pageHashParams.page}>
        {{
          undefined: () => <div>Root</div>,
          '': () => <div>Page</div>,
          Home: () => <div>Home</div>,
          default: () => <div>Unknown page</div>,
        }}
      </Switch>
    </div>
  );
}
Renders: 1
Hash:
Root