@tldraw/state-react 5.2.0-canary.e22474d279fb → 5.2.0-canary.ed81413e0a67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DOCS.md ADDED
@@ -0,0 +1,819 @@
1
+ # @tldraw/state-react Documentation
2
+
3
+ ## 1. Introduction
4
+
5
+ ### What is @tldraw/state-react?
6
+
7
+ @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.
8
+
9
+ 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.
10
+
11
+ ### Installation
12
+
13
+ ```bash
14
+ npm install @tldraw/state-react @tldraw/state react
15
+ ```
16
+
17
+ ### TypeScript
18
+
19
+ @tldraw/state-react is written in TypeScript and provides excellent type safety out of the box. No additional types package needed.
20
+
21
+ ### Quick Example
22
+
23
+ Here's a simple counter to show how @tldraw/state-react works:
24
+
25
+ ```ts
26
+ import { useAtom, track } from '@tldraw/state-react'
27
+
28
+ const Counter = track(function Counter() {
29
+ const count = useAtom('count', 0)
30
+
31
+ return (
32
+ <button onClick={() => count.set(count.get() + 1)}>
33
+ Count: {count.get()}
34
+ </button>
35
+ )
36
+ })
37
+ ```
38
+
39
+ The `track` function automatically detects when signals are accessed and re-renders the component only when those specific signals change.
40
+
41
+ ## 2. Core Hooks
42
+
43
+ ### useValue: Reading Signal Values
44
+
45
+ **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.
46
+
47
+ #### Reading Signal Values
48
+
49
+ You can use `useValue` to extract the current value from any signal:
50
+
51
+ ```ts
52
+ import { atom } from '@tldraw/state'
53
+ import { useValue } from '@tldraw/state-react'
54
+
55
+ const name = atom('name', 'World')
56
+
57
+ function Greeter() {
58
+ const currentName = useValue(name)
59
+ return <h1>Hello, {currentName}!</h1>
60
+ }
61
+ ```
62
+
63
+ When `name` changes, the `Greeter` component automatically re-renders with the new value.
64
+
65
+ #### Creating Computed Values
66
+
67
+ You can also pass a function to `useValue` to create computed values with automatic dependency tracking:
68
+
69
+ ```ts
70
+ import { atom } from '@tldraw/state'
71
+ import { useValue } from '@tldraw/state-react'
72
+
73
+ const firstName = atom('firstName', 'John')
74
+ const lastName = atom('lastName', 'Doe')
75
+
76
+ function UserProfile() {
77
+ const fullName = useValue('fullName', () => {
78
+ return `${firstName.get()} ${lastName.get()}`
79
+ }, [firstName, lastName])
80
+
81
+ return <div>User: {fullName}</div>
82
+ }
83
+ ```
84
+
85
+ > Tip: The dependency array works like other React hooks - include all signals that your computed function depends on.
86
+
87
+ ### useAtom: Component-Local Reactive State
88
+
89
+ **useAtom** creates reactive atoms that are scoped to your component instance. This is perfect for component-local state that you want to be reactive.
90
+
91
+ #### Creating Local Atoms
92
+
93
+ ```ts
94
+ import { useAtom } from '@tldraw/state-react'
95
+
96
+ function TodoItem() {
97
+ const completed = useAtom('completed', false)
98
+ const text = useAtom('text', 'New todo')
99
+
100
+ return (
101
+ <div>
102
+ <input
103
+ type="checkbox"
104
+ checked={completed.get()}
105
+ onChange={(e) => completed.set(e.target.checked)}
106
+ />
107
+ <input
108
+ value={text.get()}
109
+ onChange={(e) => text.set(e.target.value)}
110
+ />
111
+ </div>
112
+ )
113
+ }
114
+ ```
115
+
116
+ #### Lazy Initialization
117
+
118
+ You can pass a function as the initial value for expensive computations:
119
+
120
+ ```ts
121
+ function DataProcessor() {
122
+ const expensiveData = useAtom('data', () => {
123
+ // This function only runs once when the component mounts
124
+ return processLargeDataset()
125
+ })
126
+
127
+ return <div>Processing {expensiveData.get().length} items</div>
128
+ }
129
+ ```
130
+
131
+ #### Atom Options
132
+
133
+ You can customize atom behavior with options:
134
+
135
+ ```ts
136
+ const user = useAtom(
137
+ 'user',
138
+ { id: 1, name: 'Alice' },
139
+ {
140
+ isEqual: (a, b) => a.id === b.id, // Only update if ID changes
141
+ }
142
+ )
143
+ ```
144
+
145
+ ### useComputed: Component-Local Computed Values
146
+
147
+ **useComputed** creates computed signals that automatically track their dependencies and recalculate when needed.
148
+
149
+ #### Basic Computed Values
150
+
151
+ ```ts
152
+ import { useAtom, useComputed } from '@tldraw/state-react'
153
+
154
+ function ShoppingCart() {
155
+ const items = useAtom('items', [])
156
+ const total = useComputed('total', () => {
157
+ return items.get().reduce((sum, item) => sum + item.price, 0)
158
+ }, [items])
159
+
160
+ return <div>Total: ${total.get().toFixed(2)}</div>
161
+ }
162
+ ```
163
+
164
+ #### Advanced Computed Options
165
+
166
+ You can provide options for custom equality checking and diff computation:
167
+
168
+ ```ts
169
+ const optimizedData = useComputed(
170
+ 'processed',
171
+ () => {
172
+ return heavyProcessing(rawData.get())
173
+ },
174
+ {
175
+ isEqual: (a, b) => a.checksum === b.checksum,
176
+ },
177
+ [rawData]
178
+ )
179
+ ```
180
+
181
+ > Tip: Use computed values to avoid expensive recalculations. They only recalculate when their dependencies actually change.
182
+
183
+ ## 3. Component Tracking
184
+
185
+ ### track: Automatic Signal Tracking
186
+
187
+ 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.
188
+
189
+ #### Basic Component Tracking
190
+
191
+ ```ts
192
+ import { atom } from '@tldraw/state'
193
+ import { track } from '@tldraw/state-react'
194
+
195
+ const theme = atom('theme', 'light')
196
+ const userName = atom('userName', 'Guest')
197
+
198
+ const Header = track(function Header() {
199
+ return (
200
+ <header className={theme.get()}>
201
+ Welcome, {userName.get()}!
202
+ </header>
203
+ )
204
+ })
205
+ ```
206
+
207
+ Now whenever `theme` or `userName` changes, the `Header` component automatically re-renders.
208
+
209
+ #### Tracking with Props
210
+
211
+ Track works seamlessly with component props and React patterns:
212
+
213
+ ```ts
214
+ interface UserCardProps {
215
+ userId: string
216
+ }
217
+
218
+ const UserCard = track(function UserCard({ userId }: UserCardProps) {
219
+ const user = useValue('user', () => getUserById(userId), [userId])
220
+
221
+ return (
222
+ <div>
223
+ <h3>{user.name}</h3>
224
+ <p>Email: {user.email}</p>
225
+ </div>
226
+ )
227
+ })
228
+ ```
229
+
230
+ #### Tracking and React.memo
231
+
232
+ The `track` function automatically wraps your component in `React.memo`, so it only re-renders when props change or tracked signals change:
233
+
234
+ ```ts
235
+ // This component only re-renders when:
236
+ // 1. The userId prop changes, OR
237
+ // 2. The signals accessed inside the component change
238
+ const OptimizedUserCard = track(function OptimizedUserCard({ userId }: UserCardProps) {
239
+ const user = getUserAtom(userId) // Signal access is tracked
240
+ return <div>{user.get().name}</div>
241
+ })
242
+ ```
243
+
244
+ ### useStateTracking: Manual Tracking
245
+
246
+ 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.
247
+
248
+ ```ts
249
+ import { useStateTracking } from '@tldraw/state-react'
250
+
251
+ function CustomComponent() {
252
+ const [regularState, setRegularState] = useState(0)
253
+
254
+ const reactiveContent = useStateTracking('reactive-section', () => {
255
+ // Only this part is reactive to signals
256
+ return <div>Current theme: {theme.get()}</div>
257
+ }, []) // deps array is optional
258
+
259
+ return (
260
+ <div>
261
+ <button onClick={() => setRegularState(s => s + 1)}>
262
+ Regular state: {regularState}
263
+ </button>
264
+ {reactiveContent}
265
+ </div>
266
+ )
267
+ }
268
+ ```
269
+
270
+ > Tip: Use `useStateTracking` when you need fine-grained control over which parts of your component are reactive.
271
+
272
+ ## 4. Side Effects and Reactions
273
+
274
+ 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.
275
+
276
+ ### useReactor: Frame-Throttled Effects
277
+
278
+ **useReactor** runs side effects in response to signal changes, with updates throttled to animation frames for optimal performance:
279
+
280
+ ```ts
281
+ import { useReactor } from '@tldraw/state-react'
282
+
283
+ function CanvasRenderer() {
284
+ const shapes = useAtom('shapes', [])
285
+
286
+ useReactor('canvas-update', () => {
287
+ // This runs at most once per animation frame
288
+ redrawCanvas(shapes.get())
289
+ }, [shapes])
290
+
291
+ return <canvas ref={canvasRef} />
292
+ }
293
+ ```
294
+
295
+ The effect runs immediately when the component mounts, then again whenever `shapes` changes, but updates are batched to animation frames for smooth performance.
296
+
297
+ #### Visual Updates and Animations
298
+
299
+ Use `useReactor` for any visual updates or DOM manipulations:
300
+
301
+ ```ts
302
+ function AnimatedCounter() {
303
+ const count = useAtom('count', 0)
304
+ const elementRef = useRef<HTMLDivElement>(null)
305
+
306
+ useReactor('animate-color', () => {
307
+ const element = elementRef.current
308
+ if (element) {
309
+ // Animate background color based on count
310
+ element.style.backgroundColor = count.get() > 10 ? 'green' : 'blue'
311
+ }
312
+ }, [count])
313
+
314
+ return (
315
+ <div ref={elementRef}>
316
+ <button onClick={() => count.set(count.get() + 1)}>
317
+ Count: {count.get()}
318
+ </button>
319
+ </div>
320
+ )
321
+ }
322
+ ```
323
+
324
+ ### useQuickReactor: Immediate Effects
325
+
326
+ **useQuickReactor** runs side effects immediately without throttling, perfect for critical updates that can't wait:
327
+
328
+ ```ts
329
+ import { useQuickReactor } from '@tldraw/state-react'
330
+
331
+ function DataSynchronizer() {
332
+ const criticalData = useAtom('criticalData', null)
333
+
334
+ useQuickReactor('sync-data', () => {
335
+ const data = criticalData.get()
336
+ if (data) {
337
+ // Send immediately - don't wait for next frame
338
+ sendToServer(data)
339
+ }
340
+ }, [criticalData])
341
+
342
+ return <div>Sync status updated</div>
343
+ }
344
+ ```
345
+
346
+ #### When to Use Quick vs Throttled Effects
347
+
348
+ **Use `useReactor` (throttled) for:**
349
+
350
+ - Visual updates and animations
351
+ - DOM manipulations
352
+ - Canvas rendering
353
+ - UI state changes
354
+
355
+ **Use `useQuickReactor` (immediate) for:**
356
+
357
+ - Data synchronization
358
+ - Network requests
359
+ - Critical state updates
360
+ - Event logging
361
+
362
+ ```ts
363
+ function ComprehensiveExample() {
364
+ const userInput = useAtom('userInput', '')
365
+ const selectedItems = useAtom('selectedItems', [])
366
+
367
+ // Throttled: Visual feedback
368
+ useReactor('visual-feedback', () => {
369
+ updateHighlightedElements(selectedItems.get())
370
+ }, [selectedItems])
371
+
372
+ // Immediate: Data persistence
373
+ useQuickReactor('save-draft', () => {
374
+ saveDraft(userInput.get())
375
+ }, [userInput])
376
+
377
+ return (
378
+ <div>
379
+ <input onChange={(e) => userInput.set(e.target.value)} />
380
+ {/* ... */}
381
+ </div>
382
+ )
383
+ }
384
+ ```
385
+
386
+ ## 5. Advanced Patterns
387
+
388
+ ### Performance Optimization
389
+
390
+ While @tldraw/state-react is optimized by default, there are patterns for demanding applications.
391
+
392
+ #### Minimizing Re-renders with Selective Tracking
393
+
394
+ Use `track` strategically to only make components reactive when needed:
395
+
396
+ ```ts
397
+ // Only the inner component is reactive
398
+ function UserDashboard({ userId }: Props) {
399
+ return (
400
+ <div>
401
+ <StaticHeader />
402
+ <UserContent userId={userId} /> {/* This is tracked */}
403
+ <StaticFooter />
404
+ </div>
405
+ )
406
+ }
407
+
408
+ const UserContent = track(function UserContent({ userId }: Props) {
409
+ const user = getUserSignal(userId)
410
+ return <div>{user.get().name}</div>
411
+ })
412
+ ```
413
+
414
+ #### Batching Updates with Multiple Signals
415
+
416
+ When you need to update multiple related signals, use transactions from @tldraw/state:
417
+
418
+ ```ts
419
+ import { transact } from '@tldraw/state'
420
+
421
+ function BulkUpdater() {
422
+ const firstName = useAtom('firstName', '')
423
+ const lastName = useAtom('lastName', '')
424
+ const email = useAtom('email', '')
425
+
426
+ const updateUser = (userData: UserData) => {
427
+ transact(() => {
428
+ // All three updates happen atomically
429
+ firstName.set(userData.firstName)
430
+ lastName.set(userData.lastName)
431
+ email.set(userData.email)
432
+ })
433
+ // Components re-render only once after all changes
434
+ }
435
+
436
+ return <button onClick={() => updateUser(newData)}>Update User</button>
437
+ }
438
+ ```
439
+
440
+ ### Integration with External State
441
+
442
+ #### Syncing with External Systems
443
+
444
+ You can use reactive effects to sync with external systems:
445
+
446
+ ```ts
447
+ function LocalStorageSync() {
448
+ const preferences = useAtom('preferences', {})
449
+
450
+ // Save to localStorage when preferences change
451
+ useQuickReactor('save-preferences', () => {
452
+ localStorage.setItem('prefs', JSON.stringify(preferences.get()))
453
+ }, [preferences])
454
+
455
+ // Load from localStorage on mount
456
+ useEffect(() => {
457
+ const saved = localStorage.getItem('prefs')
458
+ if (saved) {
459
+ preferences.set(JSON.parse(saved))
460
+ }
461
+ }, [])
462
+
463
+ return <div>Preferences synced!</div>
464
+ }
465
+ ```
466
+
467
+ #### WebSocket Integration
468
+
469
+ ```ts
470
+ function RealtimeData() {
471
+ const liveData = useAtom('liveData', {})
472
+
473
+ useEffect(() => {
474
+ const ws = new WebSocket('ws://localhost:8080')
475
+
476
+ ws.onmessage = (event) => {
477
+ const data = JSON.parse(event.data)
478
+ liveData.set(data) // Updates trigger reactive re-renders
479
+ }
480
+
481
+ return () => ws.close()
482
+ }, [])
483
+
484
+ return <div>Live data: {JSON.stringify(liveData.get())}</div>
485
+ }
486
+ ```
487
+
488
+ ### Custom Hook Patterns
489
+
490
+ You can create custom hooks that combine multiple state-react hooks:
491
+
492
+ ```ts
493
+ function useCounter(initialValue = 0) {
494
+ const count = useAtom('count', initialValue)
495
+
496
+ const increment = useCallback(() => count.update(n => n + 1), [count])
497
+ const decrement = useCallback(() => count.update(n => n - 1), [count])
498
+ const reset = useCallback(() => count.set(initialValue), [count, initialValue])
499
+
500
+ return {
501
+ count: count.get(),
502
+ increment,
503
+ decrement,
504
+ reset
505
+ }
506
+ }
507
+
508
+ // Usage
509
+ const CounterComponent = track(function CounterComponent() {
510
+ const { count, increment, decrement, reset } = useCounter(10)
511
+
512
+ return (
513
+ <div>
514
+ <span>{count}</span>
515
+ <button onClick={increment}>+</button>
516
+ <button onClick={decrement}>-</button>
517
+ <button onClick={reset}>Reset</button>
518
+ </div>
519
+ )
520
+ })
521
+ ```
522
+
523
+ ## 6. Debugging and Development
524
+
525
+ Because @tldraw/state-react builds on the reactive signals from @tldraw/state, you get access to powerful debugging tools for understanding component behavior.
526
+
527
+ ### Using whyAmIRunning
528
+
529
+ You can use `whyAmIRunning()` from @tldraw/state to debug why components are re-rendering:
530
+
531
+ ```ts
532
+ import { whyAmIRunning } from '@tldraw/state'
533
+
534
+ const DebuggableComponent = track(function DebuggableComponent() {
535
+ const userStatus = useValue(currentUser, user => user.status, [currentUser])
536
+ const themeColor = useValue(appTheme, theme => theme.primaryColor, [appTheme])
537
+
538
+ // Debug why this component is re-rendering
539
+ if (process.env.NODE_ENV === 'development') {
540
+ whyAmIRunning()
541
+ }
542
+
543
+ return (
544
+ <div style={{ color: themeColor }}>
545
+ Status: {userStatus}
546
+ </div>
547
+ )
548
+ })
549
+ ```
550
+
551
+ When the component re-renders, you'll see output like:
552
+
553
+ ```
554
+ TrackedComponent is executing because:
555
+ ↳ Computed(user.status) changed
556
+ ↳ Atom(currentUser) changed
557
+ ```
558
+
559
+ ### Debugging Reactive Effects
560
+
561
+ You can debug reactive effects by adding logging:
562
+
563
+ ```ts
564
+ function DebuggableEffects() {
565
+ const data = useAtom('data', [])
566
+
567
+ useReactor('debug-data-changes', () => {
568
+ console.log('Data changed:', data.get())
569
+ console.log('Change triggered at:', new Date().toISOString())
570
+
571
+ // Use whyAmIRunning to see what caused this effect
572
+ if (process.env.NODE_ENV === 'development') {
573
+ whyAmIRunning()
574
+ }
575
+ }, [data])
576
+
577
+ return <div>Check console for debug info</div>
578
+ }
579
+ ```
580
+
581
+ ### Component Performance Monitoring
582
+
583
+ Track renders and signal accesses in development:
584
+
585
+ ```ts
586
+ const MonitoredComponent = track(function MonitoredComponent({ userId }: Props) {
587
+ if (process.env.NODE_ENV === 'development') {
588
+ console.log('Component rendering for user:', userId)
589
+ }
590
+
591
+ const user = useValue('user', () => {
592
+ console.log('Fetching user data...')
593
+ return getUserById(userId)
594
+ }, [userId])
595
+
596
+ if (process.env.NODE_ENV === 'development') {
597
+ console.log('User data:', user)
598
+ }
599
+
600
+ return <div>User: {user.name}</div>
601
+ })
602
+ ```
603
+
604
+ ### React DevTools Integration
605
+
606
+ @tldraw/state-react works seamlessly with React DevTools:
607
+
608
+ - Components wrapped with `track` appear as "Memo(ComponentName)"
609
+ - Signal updates trigger normal React re-render detection
610
+ - Props changes are tracked separately from signal changes
611
+ - Use React DevTools Profiler to identify performance bottlenecks
612
+
613
+ > Tip: Enable "Highlight when components render" in React DevTools to see which components re-render when signals change.
614
+
615
+ ## 7. Best Practices and Patterns
616
+
617
+ ### Component Organization
618
+
619
+ Structure your reactive components for maintainability:
620
+
621
+ ```ts
622
+ // ❌ Avoid: Large components with mixed concerns
623
+ const MonolithicComponent = track(function MonolithicComponent() {
624
+ const user = useAtom('user', {})
625
+ const posts = useAtom('posts', [])
626
+ const comments = useAtom('comments', [])
627
+ const theme = useAtom('theme', 'light')
628
+
629
+ // 100+ lines of mixed logic...
630
+ })
631
+
632
+ // ✅ Better: Split into focused components
633
+ const UserProfile = track(function UserProfile() {
634
+ const user = useAtom('user', {})
635
+ return <UserInfo user={user.get()} />
636
+ })
637
+
638
+ const PostsList = track(function PostsList() {
639
+ const posts = useAtom('posts', [])
640
+ return <PostList posts={posts.get()} />
641
+ })
642
+ ```
643
+
644
+ ### Signal Naming Conventions
645
+
646
+ Use consistent naming for clarity:
647
+
648
+ ```ts
649
+ // ✅ Good: Descriptive names with context
650
+ const currentUser = useAtom('currentUser', null)
651
+ const selectedShapes = useAtom('selectedShapes', [])
652
+ const editorMode = useAtom('editorMode', 'select')
653
+
654
+ // ❌ Avoid: Generic names without context
655
+ const data = useAtom('data', null)
656
+ const state = useAtom('state', {})
657
+ const items = useAtom('items', [])
658
+ ```
659
+
660
+ ### Effect Organization
661
+
662
+ Keep effects focused and well-named:
663
+
664
+ ```ts
665
+ function WellOrganizedComponent() {
666
+ const shapes = useAtom('shapes', [])
667
+ const camera = useAtom('camera', { x: 0, y: 0, z: 1 })
668
+
669
+ // Visual updates - throttled
670
+ useReactor('update-viewport', () => {
671
+ updateViewportTransform(camera.get())
672
+ }, [camera])
673
+
674
+ useReactor('render-shapes', () => {
675
+ renderShapes(shapes.get())
676
+ }, [shapes])
677
+
678
+ // Data persistence - immediate
679
+ useQuickReactor('save-document', () => {
680
+ saveDocument({ shapes: shapes.get(), camera: camera.get() })
681
+ }, [shapes, camera])
682
+
683
+ return <canvas />
684
+ }
685
+ ```
686
+
687
+ ### Error Handling
688
+
689
+ Handle errors gracefully in reactive code:
690
+
691
+ ```ts
692
+ const SafeDataComponent = track(function SafeDataComponent() {
693
+ const [error, setError] = useState(null)
694
+
695
+ const userData = useValue('userData', () => {
696
+ try {
697
+ return processUserData(rawUserData.get())
698
+ } catch (err) {
699
+ setError(err)
700
+ return null
701
+ }
702
+ }, [rawUserData])
703
+
704
+ if (error) {
705
+ return <ErrorMessage error={error} />
706
+ }
707
+
708
+ return <UserDisplay data={userData} />
709
+ })
710
+ ```
711
+
712
+ ### Testing Reactive Components
713
+
714
+ Test reactive components by testing signal changes:
715
+
716
+ ```ts
717
+ import { render, act } from '@testing-library/react'
718
+ import { atom } from '@tldraw/state'
719
+
720
+ test('component updates when signal changes', () => {
721
+ const nameSignal = atom('name', 'Initial')
722
+
723
+ const TestComponent = track(function TestComponent() {
724
+ return <div data-testid="name">{nameSignal.get()}</div>
725
+ })
726
+
727
+ const { getByTestId } = render(<TestComponent />)
728
+
729
+ expect(getByTestId('name')).toHaveTextContent('Initial')
730
+
731
+ act(() => {
732
+ nameSignal.set('Updated')
733
+ })
734
+
735
+ expect(getByTestId('name')).toHaveTextContent('Updated')
736
+ })
737
+ ```
738
+
739
+ ## 8. Integration with @tldraw/state
740
+
741
+ @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.
742
+
743
+ ### Using Atoms and Computed Values
744
+
745
+ ```ts
746
+ import { atom, computed } from '@tldraw/state'
747
+ import { track, useValue } from '@tldraw/state-react'
748
+
749
+ // Create signals outside of components for global state
750
+ const firstName = atom('firstName', 'John')
751
+ const lastName = atom('lastName', 'Doe')
752
+ const fullName = computed('fullName', () => `${firstName.get()} ${lastName.get()}`)
753
+
754
+ const UserGreeting = track(function UserGreeting() {
755
+ // Access global signals in components
756
+ const name = fullName.get()
757
+ return <h1>Hello, {name}!</h1>
758
+ })
759
+ ```
760
+
761
+ ### Transactions and Batching
762
+
763
+ Use transactions to batch multiple updates:
764
+
765
+ ```ts
766
+ import { transact } from '@tldraw/state'
767
+
768
+ function BatchedUpdates() {
769
+ const user = useAtom('user', { name: '', email: '' })
770
+
771
+ const updateUser = (newData: UserData) => {
772
+ transact(() => {
773
+ // Multiple updates happen atomically
774
+ user.update(current => ({ ...current, name: newData.name }))
775
+ user.update(current => ({ ...current, email: newData.email }))
776
+ })
777
+ // Component only re-renders once after both changes
778
+ }
779
+
780
+ return <button onClick={() => updateUser(formData)}>Update</button>
781
+ }
782
+ ```
783
+
784
+ ### History and Time Travel
785
+
786
+ Access signal history for undo/redo functionality:
787
+
788
+ ```ts
789
+ import { atom } from '@tldraw/state'
790
+ import { useAtom } from '@tldraw/state-react'
791
+
792
+ function UndoableEditor() {
793
+ // Create an atom with history enabled
794
+ const content = useAtom('content', '', { historyLength: 10 })
795
+
796
+ const undo = () => {
797
+ // The history is stored on the atom and can be accessed
798
+ // to implement undo/redo functionality. The exact implementation
799
+ // depends on your diff format and how you use the history API
800
+ // from @tldraw/state.
801
+ // e.g. `const diffs = content.getDiffSince(someEpoch)`
802
+ console.log('Undo clicked. See @tldraw/state docs for implementation.')
803
+ }
804
+
805
+ return (
806
+ <div>
807
+ <textarea
808
+ value={content.get()}
809
+ onChange={(e) => content.set(e.target.value)}
810
+ />
811
+ <button onClick={undo}>Undo</button>
812
+ </div>
813
+ )
814
+ }
815
+ ```
816
+
817
+ > Tip: See the [@tldraw/state documentation](../state/DOCS.md) for complete details on transactions, history, and advanced signal features.
818
+
819
+ This powerful combination of @tldraw/state and @tldraw/state-react gives you a complete reactive state solution that scales from simple components to complex applications like the tldraw editor itself.
package/README.md CHANGED
@@ -4,9 +4,17 @@ React bindings for tldraw's signals library. See also the [signals library](http
4
4
 
5
5
  To learn more about this check out the [Signia library's React bindings](https://signia.tldraw.dev/docs/react-bindings) which explains the bindings and also talks about the foundational concepts.
6
6
 
7
+ ## Documentation
8
+
9
+ Documentation for the most recent release can be found on [tldraw.dev/docs](https://tldraw.dev/docs), including [reference docs](https://tldraw.dev/reference/editor/Editor). Our release notes can be found [here](https://tldraw.dev/releases).
10
+
11
+ For more agent-friendly docs, see our [LLMs.txt](https://tldraw.dev/llms.txt).
12
+
13
+ A `DOCS.md` file is included alongside this README in the published package, with detailed API documentation and usage examples.
14
+
7
15
  ## Contribution
8
16
 
9
- Please see our [contributing guide](https://github.com/tldraw/tldraw/blob/main/CONTRIBUTING.md). Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new).
17
+ Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new).
10
18
 
11
19
  ## License
12
20
 
package/dist-cjs/index.js CHANGED
@@ -37,7 +37,7 @@ var import_useStateTracking = require("./lib/useStateTracking");
37
37
  var import_useValue = require("./lib/useValue");
38
38
  (0, import_utils.registerTldrawLibraryVersion)(
39
39
  "@tldraw/state-react",
40
- "5.2.0-canary.e22474d279fb",
40
+ "5.2.0-canary.ed81413e0a67",
41
41
  "cjs"
42
42
  );
43
43
  //# sourceMappingURL=index.js.map
@@ -8,7 +8,7 @@ import { useStateTracking } from "./lib/useStateTracking.mjs";
8
8
  import { useValue } from "./lib/useValue.mjs";
9
9
  registerTldrawLibraryVersion(
10
10
  "@tldraw/state-react",
11
- "5.2.0-canary.e22474d279fb",
11
+ "5.2.0-canary.ed81413e0a67",
12
12
  "esm"
13
13
  );
14
14
  export {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/state-react",
3
3
  "description": "tldraw infinite canvas SDK (react bindings for state).",
4
- "version": "5.2.0-canary.e22474d279fb",
4
+ "version": "5.2.0-canary.ed81413e0a67",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -29,7 +29,8 @@
29
29
  "files": [
30
30
  "dist-esm",
31
31
  "dist-cjs",
32
- "src"
32
+ "src",
33
+ "DOCS.md"
33
34
  ],
34
35
  "scripts": {
35
36
  "test-ci": "yarn run -T vitest run --passWithNoTests",
@@ -43,8 +44,8 @@
43
44
  "lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
44
45
  },
45
46
  "dependencies": {
46
- "@tldraw/state": "5.2.0-canary.e22474d279fb",
47
- "@tldraw/utils": "5.2.0-canary.e22474d279fb"
47
+ "@tldraw/state": "5.2.0-canary.ed81413e0a67",
48
+ "@tldraw/utils": "5.2.0-canary.ed81413e0a67"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@testing-library/dom": "^10.0.0",
@@ -53,7 +54,7 @@
53
54
  "@types/react-dom": "^19.2.3",
54
55
  "react": "^19.2.1",
55
56
  "react-dom": "^19.2.1",
56
- "vitest": "^3.2.4"
57
+ "vitest": "^4.1.7"
57
58
  },
58
59
  "peerDependencies": {
59
60
  "react": "^18.2.0 || ^19.2.1",
@@ -6,7 +6,7 @@ import { useAtom } from './useAtom'
6
6
  import { useQuickReactor } from './useQuickReactor'
7
7
 
8
8
  describe('useQuickReactor', () => {
9
- let mockEffectFn: ReturnType<typeof vi.fn>
9
+ let mockEffectFn: ReturnType<typeof vi.fn<(...args: any[]) => any>>
10
10
  let _component: () => React.JSX.Element
11
11
  let view: RenderResult
12
12
 
@@ -6,7 +6,7 @@ import { useAtom } from './useAtom'
6
6
  import { useReactor } from './useReactor'
7
7
 
8
8
  describe('useReactor', () => {
9
- let mockEffectFn: ReturnType<typeof vi.fn>
9
+ let mockEffectFn: ReturnType<typeof vi.fn<(...args: any[]) => any>>
10
10
  let view: RenderResult
11
11
 
12
12
  beforeEach(() => {