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