# @tldraw/state-react Documentation ## 1. Introduction ### What is @tldraw/state-react? @tldraw/state-react is a React integration library that bridges the reactive **signals** system from @tldraw/state with React components. It provides hooks and utilities for using reactive state in React applications with automatic dependency tracking and fine-grained updates. This library extends [@tldraw/state](../state/DOCS.md) with React-specific bindings, offering familiar hook patterns while maintaining the performance characteristics of the underlying signals system. ### Installation ```bash npm install @tldraw/state-react @tldraw/state react ``` ### TypeScript @tldraw/state-react is written in TypeScript and provides excellent type safety out of the box. No additional types package needed. ### Quick Example Here's a simple counter to show how @tldraw/state-react works: ```ts import { useAtom, track } from '@tldraw/state-react' const Counter = track(function Counter() { const count = useAtom('count', 0) return ( ) }) ``` The `track` function automatically detects when signals are accessed and re-renders the component only when those specific signals change. ## 2. Core Hooks ### useValue: Reading Signal Values **useValue** is the fundamental hook for extracting values from signals and subscribing to changes. It comes in two forms: direct signal subscription and computed values. #### Reading Signal Values You can use `useValue` to extract the current value from any signal: ```ts import { atom } from '@tldraw/state' import { useValue } from '@tldraw/state-react' const name = atom('name', 'World') function Greeter() { const currentName = useValue(name) return

Hello, {currentName}!

} ``` When `name` changes, the `Greeter` component automatically re-renders with the new value. #### Creating Computed Values You can also pass a function to `useValue` to create computed values with automatic dependency tracking: ```ts import { atom } from '@tldraw/state' import { useValue } from '@tldraw/state-react' const firstName = atom('firstName', 'John') const lastName = atom('lastName', 'Doe') function UserProfile() { const fullName = useValue('fullName', () => { return `${firstName.get()} ${lastName.get()}` }, [firstName, lastName]) return
User: {fullName}
} ``` > Tip: The dependency array works like other React hooks - include all signals that your computed function depends on. ### useAtom: Component-Local Reactive State **useAtom** creates reactive atoms that are scoped to your component instance. This is perfect for component-local state that you want to be reactive. #### Creating Local Atoms ```ts import { useAtom } from '@tldraw/state-react' function TodoItem() { const completed = useAtom('completed', false) const text = useAtom('text', 'New todo') return (
completed.set(e.target.checked)} /> text.set(e.target.value)} />
) } ``` #### Lazy Initialization You can pass a function as the initial value for expensive computations: ```ts function DataProcessor() { const expensiveData = useAtom('data', () => { // This function only runs once when the component mounts return processLargeDataset() }) return
Processing {expensiveData.get().length} items
} ``` #### Atom Options You can customize atom behavior with options: ```ts const user = useAtom( 'user', { id: 1, name: 'Alice' }, { isEqual: (a, b) => a.id === b.id, // Only update if ID changes } ) ``` ### useComputed: Component-Local Computed Values **useComputed** creates computed signals that automatically track their dependencies and recalculate when needed. #### Basic Computed Values ```ts import { useAtom, useComputed } from '@tldraw/state-react' function ShoppingCart() { const items = useAtom('items', []) const total = useComputed('total', () => { return items.get().reduce((sum, item) => sum + item.price, 0) }, [items]) return
Total: ${total.get().toFixed(2)}
} ``` #### Advanced Computed Options You can provide options for custom equality checking and diff computation: ```ts const optimizedData = useComputed( 'processed', () => { return heavyProcessing(rawData.get()) }, { isEqual: (a, b) => a.checksum === b.checksum, }, [rawData] ) ``` > Tip: Use computed values to avoid expensive recalculations. They only recalculate when their dependencies actually change. ## 3. Component Tracking ### track: Automatic Signal Tracking The **track** higher-order component is the most convenient way to make React components reactive. It automatically tracks which signals your component accesses and re-renders when any of them change. #### Basic Component Tracking ```ts import { atom } from '@tldraw/state' import { track } from '@tldraw/state-react' const theme = atom('theme', 'light') const userName = atom('userName', 'Guest') const Header = track(function Header() { return (
Welcome, {userName.get()}!
) }) ``` Now whenever `theme` or `userName` changes, the `Header` component automatically re-renders. #### Tracking with Props Track works seamlessly with component props and React patterns: ```ts interface UserCardProps { userId: string } const UserCard = track(function UserCard({ userId }: UserCardProps) { const user = useValue('user', () => getUserById(userId), [userId]) return (

{user.name}

Email: {user.email}

) }) ``` #### Tracking and React.memo The `track` function automatically wraps your component in `React.memo`, so it only re-renders when props change or tracked signals change: ```ts // This component only re-renders when: // 1. The userId prop changes, OR // 2. The signals accessed inside the component change const OptimizedUserCard = track(function OptimizedUserCard({ userId }: UserCardProps) { const user = getUserAtom(userId) // Signal access is tracked return
{user.get().name}
}) ``` ### useStateTracking: Manual Tracking For more control, you can use **useStateTracking** to manually track signal dependencies in specific parts of your render function. It also accepts an optional dependency array, similar to `useMemo`, to control when the reactive tracking logic is re-created. ```ts import { useStateTracking } from '@tldraw/state-react' function CustomComponent() { const [regularState, setRegularState] = useState(0) const reactiveContent = useStateTracking('reactive-section', () => { // Only this part is reactive to signals return
Current theme: {theme.get()}
}, []) // deps array is optional return (
{reactiveContent}
) } ``` > Tip: Use `useStateTracking` when you need fine-grained control over which parts of your component are reactive. ## 4. Side Effects and Reactions Reading and displaying reactive state is only part of the story. The other part is performing **side effects** that respond to state changes - like updating the DOM, making network requests, or triggering animations. ### useReactor: Frame-Throttled Effects **useReactor** runs side effects in response to signal changes, with updates throttled to animation frames for optimal performance: ```ts import { useReactor } from '@tldraw/state-react' function CanvasRenderer() { const shapes = useAtom('shapes', []) useReactor('canvas-update', () => { // This runs at most once per animation frame redrawCanvas(shapes.get()) }, [shapes]) return } ``` The effect runs immediately when the component mounts, then again whenever `shapes` changes, but updates are batched to animation frames for smooth performance. #### Visual Updates and Animations Use `useReactor` for any visual updates or DOM manipulations: ```ts function AnimatedCounter() { const count = useAtom('count', 0) const elementRef = useRef(null) useReactor('animate-color', () => { const element = elementRef.current if (element) { // Animate background color based on count element.style.backgroundColor = count.get() > 10 ? 'green' : 'blue' } }, [count]) return (
) } ``` ### useQuickReactor: Immediate Effects **useQuickReactor** runs side effects immediately without throttling, perfect for critical updates that can't wait: ```ts import { useQuickReactor } from '@tldraw/state-react' function DataSynchronizer() { const criticalData = useAtom('criticalData', null) useQuickReactor('sync-data', () => { const data = criticalData.get() if (data) { // Send immediately - don't wait for next frame sendToServer(data) } }, [criticalData]) return
Sync status updated
} ``` #### When to Use Quick vs Throttled Effects **Use `useReactor` (throttled) for:** - Visual updates and animations - DOM manipulations - Canvas rendering - UI state changes **Use `useQuickReactor` (immediate) for:** - Data synchronization - Network requests - Critical state updates - Event logging ```ts function ComprehensiveExample() { const userInput = useAtom('userInput', '') const selectedItems = useAtom('selectedItems', []) // Throttled: Visual feedback useReactor('visual-feedback', () => { updateHighlightedElements(selectedItems.get()) }, [selectedItems]) // Immediate: Data persistence useQuickReactor('save-draft', () => { saveDraft(userInput.get()) }, [userInput]) return (
userInput.set(e.target.value)} /> {/* ... */}
) } ``` ## 5. Advanced Patterns ### Performance Optimization While @tldraw/state-react is optimized by default, there are patterns for demanding applications. #### Minimizing Re-renders with Selective Tracking Use `track` strategically to only make components reactive when needed: ```ts // Only the inner component is reactive function UserDashboard({ userId }: Props) { return (
{/* This is tracked */}
) } const UserContent = track(function UserContent({ userId }: Props) { const user = getUserSignal(userId) return
{user.get().name}
}) ``` #### Batching Updates with Multiple Signals When you need to update multiple related signals, use transactions from @tldraw/state: ```ts import { transact } from '@tldraw/state' function BulkUpdater() { const firstName = useAtom('firstName', '') const lastName = useAtom('lastName', '') const email = useAtom('email', '') const updateUser = (userData: UserData) => { transact(() => { // All three updates happen atomically firstName.set(userData.firstName) lastName.set(userData.lastName) email.set(userData.email) }) // Components re-render only once after all changes } return } ``` ### Integration with External State #### Syncing with External Systems You can use reactive effects to sync with external systems: ```ts function LocalStorageSync() { const preferences = useAtom('preferences', {}) // Save to localStorage when preferences change useQuickReactor('save-preferences', () => { localStorage.setItem('prefs', JSON.stringify(preferences.get())) }, [preferences]) // Load from localStorage on mount useEffect(() => { const saved = localStorage.getItem('prefs') if (saved) { preferences.set(JSON.parse(saved)) } }, []) return
Preferences synced!
} ``` #### WebSocket Integration ```ts function RealtimeData() { const liveData = useAtom('liveData', {}) useEffect(() => { const ws = new WebSocket('ws://localhost:8080') ws.onmessage = (event) => { const data = JSON.parse(event.data) liveData.set(data) // Updates trigger reactive re-renders } return () => ws.close() }, []) return
Live data: {JSON.stringify(liveData.get())}
} ``` ### Custom Hook Patterns You can create custom hooks that combine multiple state-react hooks: ```ts function useCounter(initialValue = 0) { const count = useAtom('count', initialValue) const increment = useCallback(() => count.update(n => n + 1), [count]) const decrement = useCallback(() => count.update(n => n - 1), [count]) const reset = useCallback(() => count.set(initialValue), [count, initialValue]) return { count: count.get(), increment, decrement, reset } } // Usage const CounterComponent = track(function CounterComponent() { const { count, increment, decrement, reset } = useCounter(10) return (
{count}
) }) ``` ## 6. Debugging and Development Because @tldraw/state-react builds on the reactive signals from @tldraw/state, you get access to powerful debugging tools for understanding component behavior. ### Using whyAmIRunning You can use `whyAmIRunning()` from @tldraw/state to debug why components are re-rendering: ```ts import { whyAmIRunning } from '@tldraw/state' const DebuggableComponent = track(function DebuggableComponent() { const userStatus = useValue(currentUser, user => user.status, [currentUser]) const themeColor = useValue(appTheme, theme => theme.primaryColor, [appTheme]) // Debug why this component is re-rendering if (process.env.NODE_ENV === 'development') { whyAmIRunning() } return (
Status: {userStatus}
) }) ``` When the component re-renders, you'll see output like: ``` TrackedComponent is executing because: ↳ Computed(user.status) changed ↳ Atom(currentUser) changed ``` ### Debugging Reactive Effects You can debug reactive effects by adding logging: ```ts function DebuggableEffects() { const data = useAtom('data', []) useReactor('debug-data-changes', () => { console.log('Data changed:', data.get()) console.log('Change triggered at:', new Date().toISOString()) // Use whyAmIRunning to see what caused this effect if (process.env.NODE_ENV === 'development') { whyAmIRunning() } }, [data]) return
Check console for debug info
} ``` ### Component Performance Monitoring Track renders and signal accesses in development: ```ts const MonitoredComponent = track(function MonitoredComponent({ userId }: Props) { if (process.env.NODE_ENV === 'development') { console.log('Component rendering for user:', userId) } const user = useValue('user', () => { console.log('Fetching user data...') return getUserById(userId) }, [userId]) if (process.env.NODE_ENV === 'development') { console.log('User data:', user) } return
User: {user.name}
}) ``` ### React DevTools Integration @tldraw/state-react works seamlessly with React DevTools: - Components wrapped with `track` appear as "Memo(ComponentName)" - Signal updates trigger normal React re-render detection - Props changes are tracked separately from signal changes - Use React DevTools Profiler to identify performance bottlenecks > Tip: Enable "Highlight when components render" in React DevTools to see which components re-render when signals change. ## 7. Best Practices and Patterns ### Component Organization Structure your reactive components for maintainability: ```ts // ❌ Avoid: Large components with mixed concerns const MonolithicComponent = track(function MonolithicComponent() { const user = useAtom('user', {}) const posts = useAtom('posts', []) const comments = useAtom('comments', []) const theme = useAtom('theme', 'light') // 100+ lines of mixed logic... }) // ✅ Better: Split into focused components const UserProfile = track(function UserProfile() { const user = useAtom('user', {}) return }) const PostsList = track(function PostsList() { const posts = useAtom('posts', []) return }) ``` ### Signal Naming Conventions Use consistent naming for clarity: ```ts // ✅ Good: Descriptive names with context const currentUser = useAtom('currentUser', null) const selectedShapes = useAtom('selectedShapes', []) const editorMode = useAtom('editorMode', 'select') // ❌ Avoid: Generic names without context const data = useAtom('data', null) const state = useAtom('state', {}) const items = useAtom('items', []) ``` ### Effect Organization Keep effects focused and well-named: ```ts function WellOrganizedComponent() { const shapes = useAtom('shapes', []) const camera = useAtom('camera', { x: 0, y: 0, z: 1 }) // Visual updates - throttled useReactor('update-viewport', () => { updateViewportTransform(camera.get()) }, [camera]) useReactor('render-shapes', () => { renderShapes(shapes.get()) }, [shapes]) // Data persistence - immediate useQuickReactor('save-document', () => { saveDocument({ shapes: shapes.get(), camera: camera.get() }) }, [shapes, camera]) return } ``` ### Error Handling Handle errors gracefully in reactive code: ```ts const SafeDataComponent = track(function SafeDataComponent() { const [error, setError] = useState(null) const userData = useValue('userData', () => { try { return processUserData(rawUserData.get()) } catch (err) { setError(err) return null } }, [rawUserData]) if (error) { return } return }) ``` ### Testing Reactive Components Test reactive components by testing signal changes: ```ts import { render, act } from '@testing-library/react' import { atom } from '@tldraw/state' test('component updates when signal changes', () => { const nameSignal = atom('name', 'Initial') const TestComponent = track(function TestComponent() { return
{nameSignal.get()}
}) const { getByTestId } = render() expect(getByTestId('name')).toHaveTextContent('Initial') act(() => { nameSignal.set('Updated') }) expect(getByTestId('name')).toHaveTextContent('Updated') }) ``` ## 8. Integration with @tldraw/state @tldraw/state-react is designed to work seamlessly with the core @tldraw/state library. You can use all the features of @tldraw/state within React components. ### Using Atoms and Computed Values ```ts import { atom, computed } from '@tldraw/state' import { track, useValue } from '@tldraw/state-react' // Create signals outside of components for global state const firstName = atom('firstName', 'John') const lastName = atom('lastName', 'Doe') const fullName = computed('fullName', () => `${firstName.get()} ${lastName.get()}`) const UserGreeting = track(function UserGreeting() { // Access global signals in components const name = fullName.get() return

Hello, {name}!

}) ``` ### Transactions and Batching Use transactions to batch multiple updates: ```ts import { transact } from '@tldraw/state' function BatchedUpdates() { const user = useAtom('user', { name: '', email: '' }) const updateUser = (newData: UserData) => { transact(() => { // Multiple updates happen atomically user.update(current => ({ ...current, name: newData.name })) user.update(current => ({ ...current, email: newData.email })) }) // Component only re-renders once after both changes } return } ``` ### History and Time Travel Access signal history for undo/redo functionality: ```ts import { atom } from '@tldraw/state' import { useAtom } from '@tldraw/state-react' function UndoableEditor() { // Create an atom with history enabled const content = useAtom('content', '', { historyLength: 10 }) const undo = () => { // The history is stored on the atom and can be accessed // to implement undo/redo functionality. The exact implementation // depends on your diff format and how you use the history API // from @tldraw/state. // e.g. `const diffs = content.getDiffSince(someEpoch)` console.log('Undo clicked. See @tldraw/state docs for implementation.') } return (