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 { syncObservable } from "@legendapp/state/sync" import { ObservablePersistLocalStorage } from "@legendapp/state/persist-plugins/local-storage" import { $React } from "@legendapp/state/react-web" import { motion } from "framer-motion" import { useRef } from "react" const state$ = observable({ settings: { showSidebar: false, theme: 'light' }, user: { profile: { name: '', avatar: '' }, messages: {} } }) // Persist state syncObservable(state$, { persist:{ name: 'persistenceExample', plugin: ObservablePersistLocalStorage, } }) // Create a reactive Framer-Motion div const MotionDiv = reactive(motion.div) function App() { const renderCount = ++useRef(0).current const sidebarHeight = () => ( state$.settings.showSidebar.get() ? 96 : 0 ) return ( <Box className="flex flex-col gap-y-3"> <div>Renders: {renderCount}</div> <div>Username:</div> <$React.input className="input" $value={state$.user.profile.name} /> <Button onClick={state$.settings.showSidebar.toggle}> Toggle footer </Button> <MotionDiv className="footer" $animate={() => ({ height: state$.settings.showSidebar.get() ? 96 : 0 })} > <div className="p-4">Footer</div> </MotionDiv> </Box> ) }
import { observable } from "@legendapp/state"
import { syncObservable } from "@legendapp/state/sync"
import { ObservablePersistLocalStorage } from "@legendapp/state/persist-plugins/local-storage"
import { $React } from "@legendapp/state/react-web"
import { motion } from "framer-motion"
import { useRef } from "react"
const state$ = observable({
settings: { showSidebar: false, theme: 'light' },
user: {
profile: { name: '', avatar: '' },
messages: {}
}
})
// Persist state
syncObservable(state$, {
persist:{
name: 'persistenceExample',
plugin: ObservablePersistLocalStorage,
}
})
// Create a reactive Framer-Motion div
const MotionDiv = reactive(motion.div)
function App() {
const renderCount = ++useRef(0).current
const sidebarHeight = () => (
state$.settings.showSidebar.get() ? 96 : 0
)
return (
<Box className="flex flex-col gap-y-3">
<div>Renders: {renderCount}</div>
<div>Username:</div>
<$React.input
className="input"
$value={state$.user.profile.name}
/>
<Button onClick={state$.settings.showSidebar.toggle}>
Toggle footer
</Button>
<MotionDiv
className="footer"
$animate={() => ({
height: state$.settings.showSidebar.get() ?
96 : 0
})}
>
<div className="p-4">Footer</div>
</MotionDiv>
</Box>
)
}Auto-saving Form
This example uses the useObservableSyncedQuery hook to create an observable using TanStack Query that automatically sends mutations back to the server whenever the observable changes.
It then uses the Reactive 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 { useObservable, Memo } from "@legendapp/state/react" import { $React } from "@legendapp/state/react-web" import { useObservableSyncedQuery } from '@legendapp/state/sync-plugins/tanstack-react-query' const queryClient = new QueryClient() function App() { return ( <QueryClientProvider client={queryClient}> <Example /> </QueryClientProvider> ) } function Example() { const renderCount = ++useRef(0).current const lastSaved$ = useObservable(0) const data$ = useObservableSyncedQuery({ queryClient, query: { queryKey: ["data"], queryFn: () => axios.get("https://reqres.in/api/users/1") .then((res) => res.data.data), }, mutation: { 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()) } } }) return ( <Box className="flex flex-col gap-y-3"> <div> Renders: {renderCount} </div> <div>Name:</div> <$React.input className="input" $value={data$.first_name} /> <div>Email:</div> <$React.input className="input" $value={data$.email} /> <div> Last saved: <Memo>{lastSaved$}</Memo> </div> </Box> ) }
import axios from "axios"
import { useRef } from "react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { useObservable, Memo } from "@legendapp/state/react"
import { $React } from "@legendapp/state/react-web"
import { useObservableSyncedQuery } from '@legendapp/state/sync-plugins/tanstack-react-query'
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
function Example() {
const renderCount = ++useRef(0).current
const lastSaved$ = useObservable(0)
const data$ = useObservableSyncedQuery({
queryClient,
query: {
queryKey: ["data"],
queryFn: () =>
axios.get("https://reqres.in/api/users/1")
.then((res) => res.data.data),
},
mutation: {
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())
}
}
})
return (
<Box className="flex flex-col gap-y-3">
<div>
Renders: {renderCount}
</div>
<div>Name:</div>
<$React.input
className="input"
$value={data$.first_name}
/>
<div>Email:</div>
<$React.input
className="input"
$value={data$.email}
/>
<div>
Last saved: <Memo>{lastSaved$}</Memo>
</div>
</Box>
)
}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 { useObservable, useObserve, Memo, Show } from "@legendapp/state/react" import { $React } from "@legendapp/state/react-web" function App() { const renderCount = ++useRef(0).current const username$ = useObservable('') const password$ = useObservable('') const usernameError$ = useObservable('') const passwordError$ = useObservable('') const didSave$ = useObservable(false) const successMessage$ = useObservable('') 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 = () => { // setting triggers useObserve, updating error messages didSave$.set(true) if (!usernameError$.get() && !passwordError$.get()) { console.log('Submit form') passwordError$.delete() successMessage$.set('Saved!') } } return ( <Box className="flex flex-col gap-y-3"> <div>Renders: {renderCount}</div> <div>Username:</div> <$React.input className="input" $value={username$} /> <div className="error"> <Memo>{usernameError$}</Memo> </div> <div>Password:</div> <$React.input type="password" className="input" $value={password$} /> <div className="error"> <Memo>{passwordError$}</Memo> </div> <Show if={successMessage$}> {() => ( <div> {successMessage$.get()} </div> )} </Show> <Button onClick={onClickSave}> Save </Button> </Box> ) }
import { useRef } from "react"
import { useObservable, useObserve, Memo, Show } from "@legendapp/state/react"
import { $React } from "@legendapp/state/react-web"
function App() {
const renderCount = ++useRef(0).current
const username$ = useObservable('')
const password$ = useObservable('')
const usernameError$ = useObservable('')
const passwordError$ = useObservable('')
const didSave$ = useObservable(false)
const successMessage$ = useObservable('')
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 = () => {
// setting triggers useObserve, updating error messages
didSave$.set(true)
if (!usernameError$.get() && !passwordError$.get()) {
console.log('Submit form')
passwordError$.delete()
successMessage$.set('Saved!')
}
}
return (
<Box className="flex flex-col gap-y-3">
<div>Renders: {renderCount}</div>
<div>Username:</div>
<$React.input
className="input"
$value={username$}
/>
<div className="error">
<Memo>{usernameError$}</Memo>
</div>
<div>Password:</div>
<$React.input
type="password"
className="input"
$value={password$}
/>
<div className="error">
<Memo>{passwordError$}</Memo>
</div>
<Show if={successMessage$}>
{() => (
<div>
{successMessage$.get()}
</div>
)}
</Show>
<Button onClick={onClickSave}>
Save
</Button>
</Box>
)
}List of messages
This example uses the syncedFetch helper 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 { For, Show, useObservable, useObservable } from "@legendapp/state/react" import { $React } from "@legendapp/state/react-web" import { syncedFetch } from "@legendapp/state/sync-plugins/fetch" let nextID = 0 function generateID() { return nextID ++ } function App() { const renderCount = ++useRef(0).current // Create profile from fetch promise const profile$ = useObservable(syncedFetch({ get: 'https://reqres.in/api/users/1' })) // Username const userName = useObservable(() => { const p = profile$.data.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 ( <Box className="flex flex-col gap-y-3"> <div>Renders: {renderCount}</div> <Show if={userName} else={<div>Loading...</div>}> <div>Chatting with <Memo>{userName}</Memo></div> </Show> <div className="messages"> <For each={messages}> {(message) => <div>{message.text.get()}</div>} </For> </div> <div className="flex gap-2 items-center"> <$React.input className="input" placeholder="Enter message" $value={currentMessage} onKeyDown={e => e.key === 'Enter' && onClickAdd()} /> <Button onClick={onClickAdd}> Send </Button> </div> </Box> ) }
import { For, Show, useObservable, useObservable } from "@legendapp/state/react"
import { $React } from "@legendapp/state/react-web"
import { syncedFetch } from "@legendapp/state/sync-plugins/fetch"
let nextID = 0
function generateID() {
return nextID ++
}
function App() {
const renderCount = ++useRef(0).current
// Create profile from fetch promise
const profile$ = useObservable(syncedFetch({
get: 'https://reqres.in/api/users/1'
}))
// Username
const userName = useObservable(() => {
const p = profile$.data.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 (
<Box className="flex flex-col gap-y-3">
<div>Renders: {renderCount}</div>
<Show if={userName} else={<div>Loading...</div>}>
<div>Chatting with <Memo>{userName}</Memo></div>
</Show>
<div className="messages">
<For each={messages}>
{(message) => <div>{message.text.get()}</div>}
</For>
</div>
<div className="flex gap-2 items-center">
<$React.input
className="input"
placeholder="Enter message"
$value={currentMessage}
onKeyDown={e => e.key === 'Enter' && onClickAdd()}
/>
<Button onClick={onClickAdd}>
Send
</Button>
</div>
</Box>
)
}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" import { useRef } from "react" import { observable } from "@legendapp/state" import { useComputed, useObservable, Memo } from "@legendapp/state/react" const MotionDiv = reactive(motion.div) function Toggle({ $value }) { return ( <MotionDiv className="toggle" $animate={() => ({ backgroundColor: $value.get() ? '#6ACB6C' : '#515153' })} style={{ width: 64, height: 32 }} onClick={$value.toggle} > <MotionDiv className="thumb" style={{ width: 24, height: 24, marginTop: 3 }} $animate={() => ({ x: $value.get() ? 34 : 4 })} /> </MotionDiv> ) } const settings$ = observable({ enabled: false }) function App() { const renderCount = ++useRef(0).current // Computed text value const text$ = useObservable(() => ( settings$.enabled.get() ? 'Yes' : 'No' )) return ( <Box className="flex flex-col gap-y-3"> <div>Renders: {renderCount}</div> <div> Enabled: <Memo>{text$}</Memo> </div> <Toggle $value={settings$.enabled} /> </Box> ) }
import { reactive } from "@legendapp/state/react"
import { motion } from "framer-motion"
import { useRef } from "react"
import { observable } from "@legendapp/state"
import { useComputed, useObservable, Memo } from "@legendapp/state/react"
const MotionDiv = reactive(motion.div)
function Toggle({ $value }) {
return (
<MotionDiv
className="toggle"
$animate={() => ({
backgroundColor: $value.get() ? '#6ACB6C' : '#515153'
})}
style={{ width: 64, height: 32 }}
onClick={$value.toggle}
>
<MotionDiv
className="thumb"
style={{ width: 24, height: 24, marginTop: 3 }}
$animate={() => ({
x: $value.get() ? 34 : 4
})}
/>
</MotionDiv>
)
}
const settings$ = observable({ enabled: false })
function App() {
const renderCount = ++useRef(0).current
// Computed text value
const text$ = useObservable(() => (
settings$.enabled.get() ? 'Yes' : 'No'
))
return (
<Box className="flex flex-col gap-y-3">
<div>Renders: {renderCount}</div>
<div>
Enabled: <Memo>{text$}</Memo>
</div>
<Toggle $value={settings$.enabled} />
</Box>
)
}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.
const MotionDiv = reactive(motion.div) const MotionButton = reactive(motion.button) const TransitionBounce = { type: 'spring', duration: 0.4, bounce: 0.3, } function Modal({ show }) { const renderCount = ++useRef(0).current const page$ = useObservable(0) return ( <motion.div className="flex items-center justify-center" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} > <div className="absolute inset-0 bg-black/60" onClick={() => show.set(false)} /> <motion.div className="modal" initial={{ opacity: 0, scale: 0.7, translateY: 40 }} animate={{ opacity: 1, scale: 1, translateY: 0 }} exit={{ scale: 0.7, opacity: 0 }} style={{ width: 240, height: 320 }} transition={TransitionBounce} > <div> Renders: {renderCount} </div> <div className="pageText"> <Switch value={page$}> {{ 0: () => <div>First Page</div>, 1: () => <div>Second Page</div>, 2: () => <div>Third Page</div> }} </Switch> </div> <div className="modalButtons"> <MotionButton className="pageButton" animate={() => ({ opacity: page$.get() === 0 ? 0.5 : 1 })} $disabled={() => page$.get() === 0} onClick={() => page$.set(p => p - 1)} transition={{ duration: 0.15 }} > Prev </MotionButton> <MotionButton className="pageButton" animate={() => ({ opacity: page$.get() === 2 ? 0.5 : 1 })} $disabled={() => page$.get() === 2} onClick={() => page$.set(p => p + 1)} transition={{ duration: 0.15 }} > Next </MotionButton> </div> </motion.div> </motion.div> ) } function App() { const renderCount = ++useRef(0).current const showModal = useObservable(false) return ( <Box height={512}> <div>Renders: {renderCount}</div> <Button onClick={showModal.toggle}> Show modal </Button> <Show if={showModal} wrap={AnimatePresence}> {() => <Modal show={showModal} />} </Show> </Box> ) }
const MotionDiv = reactive(motion.div)
const MotionButton = reactive(motion.button)
const TransitionBounce = {
type: 'spring',
duration: 0.4,
bounce: 0.3,
}
function Modal({ show }) {
const renderCount = ++useRef(0).current
const page$ = useObservable(0)
return (
<motion.div
className="flex items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<div
className="absolute inset-0 bg-black/60"
onClick={() => show.set(false)}
/>
<motion.div
className="modal"
initial={{ opacity: 0, scale: 0.7, translateY: 40 }}
animate={{ opacity: 1, scale: 1, translateY: 0 }}
exit={{ scale: 0.7, opacity: 0 }}
style={{ width: 240, height: 320 }}
transition={TransitionBounce}
>
<div>
Renders: {renderCount}
</div>
<div className="pageText">
<Switch value={page$}>
{{
0: () => <div>First Page</div>,
1: () => <div>Second Page</div>,
2: () => <div>Third Page</div>
}}
</Switch>
</div>
<div className="modalButtons">
<MotionButton
className="pageButton"
animate={() => ({ opacity: page$.get() === 0 ? 0.5 : 1 })}
$disabled={() => page$.get() === 0}
onClick={() => page$.set(p => p - 1)}
transition={{ duration: 0.15 }}
>
Prev
</MotionButton>
<MotionButton
className="pageButton"
animate={() => ({ opacity: page$.get() === 2 ? 0.5 : 1 })}
$disabled={() => page$.get() === 2}
onClick={() => page$.set(p => p + 1)}
transition={{ duration: 0.15 }}
>
Next
</MotionButton>
</div>
</motion.div>
</motion.div>
)
}
function App() {
const renderCount = ++useRef(0).current
const showModal = useObservable(false)
return (
<Box height={512}>
<div>Renders: {renderCount}</div>
<Button onClick={showModal.toggle}>
Show modal
</Button>
<Show if={showModal} wrap={AnimatePresence}>
{() => <Modal show={showModal} />}
</Show>
</Box>
)
}Router
import { useRef } from "react" import { Memo, Switch } from "@legendapp/state/react" import { pageHash } from "@legendapp/state/helpers/pageHash" import { pageHashParams } from "@legendapp/state/helpers/pageHashParams" function RouterExample() { const renderCount = ++useRef(0).current return ( <Box width={240}> <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> <div>Hash: <Memo>{pageHash}</Memo></div> <div className="p-4 bg-zinc-600 rounded-xl"> <Switch value={pageHashParams.page}> {{ undefined: () => <div>Root</div>, '': () => <div>Page</div>, Home: () => <div>Home</div>, default: () => <div>Unknown page</div>, }} </Switch> </div> </Box> ) }
import { useRef } from "react"
import { Memo, Switch } from "@legendapp/state/react"
import { pageHash } from "@legendapp/state/helpers/pageHash"
import { pageHashParams } from "@legendapp/state/helpers/pageHashParams"
function RouterExample() {
const renderCount = ++useRef(0).current
return (
<Box width={240}>
<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>
<div>Hash: <Memo>{pageHash}</Memo></div>
<div className="p-4 bg-zinc-600 rounded-xl">
<Switch value={pageHashParams.page}>
{{
undefined: () => <div>Root</div>,
'': () => <div>Page</div>,
Home: () => <div>Home</div>,
default: () => <div>Unknown page</div>,
}}
</Switch>
</div>
</Box>
)
}These examples are interactive demonstrations that would typically include live code editors and previews. In the original documentation, they were implemented as Astro components with embedded examples.