@tldraw/state-react 5.2.0-next.355d68cf24e8 → 5.2.0-next.79b13319d317
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 +819 -0
- package/README.md +5 -1
- package/dist-cjs/index.js +1 -1
- package/dist-esm/index.mjs +1 -1
- package/package.json +6 -5
- package/src/lib/useQuickReactor.test.tsx +1 -1
- package/src/lib/useReactor.test.tsx +1 -1
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,13 @@ 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
|
+
A `DOCS.md` file is included alongside this README in the published package, with detailed API documentation and usage examples.
|
|
10
|
+
|
|
7
11
|
## Contribution
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new).
|
|
10
14
|
|
|
11
15
|
## License
|
|
12
16
|
|
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-next.
|
|
40
|
+
"5.2.0-next.79b13319d317",
|
|
41
41
|
"cjs"
|
|
42
42
|
);
|
|
43
43
|
//# sourceMappingURL=index.js.map
|
package/dist-esm/index.mjs
CHANGED
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-next.
|
|
4
|
+
"version": "5.2.0-next.79b13319d317",
|
|
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-next.
|
|
47
|
-
"@tldraw/utils": "5.2.0-next.
|
|
47
|
+
"@tldraw/state": "5.2.0-next.79b13319d317",
|
|
48
|
+
"@tldraw/utils": "5.2.0-next.79b13319d317"
|
|
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": "^
|
|
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(() => {
|