@tldraw/state 5.2.0-next.b91d4a4551c9 → 5.2.0-next.cd4a35fc06d5
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 +563 -0
- package/README.md +5 -1
- package/dist-cjs/index.js +1 -1
- package/dist-esm/index.mjs +1 -1
- package/package.json +5 -4
package/DOCS.md
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
# @tldraw/state Documentation
|
|
2
|
+
|
|
3
|
+
## 1. Introduction
|
|
4
|
+
|
|
5
|
+
### What is @tldraw/state?
|
|
6
|
+
|
|
7
|
+
@tldraw/state is a powerful and lightweight TypeScript library for managing state using reactive values called **signals**. Its fine-grained reactive system allows you to build complex, performant, and predictable user interfaces and data models.
|
|
8
|
+
|
|
9
|
+
This library provides the core of tldraw's reactivity system. Its sister library, @tldraw/state-react, provides bindings for [React](https://react.dev/).
|
|
10
|
+
|
|
11
|
+
### Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @tldraw/state
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### TypeScript
|
|
18
|
+
|
|
19
|
+
@tldraw/state 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 example to show how tldraw state works:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { atom, computed, react } from '@tldraw/state'
|
|
27
|
+
|
|
28
|
+
// Create some state
|
|
29
|
+
const name = atom('name', 'World')
|
|
30
|
+
const greeting = computed('greeting', () => `Hello, ${name.get()}!`)
|
|
31
|
+
|
|
32
|
+
// React to changes
|
|
33
|
+
react('update page title', () => {
|
|
34
|
+
window.alert(greeting.get())
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Update the state
|
|
38
|
+
name.set('tldraw state')
|
|
39
|
+
// Page title automatically updates to "Hello, tldraw state!"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
In just a few lines, you've created reactive state that automatically alerts the user when it changes.
|
|
43
|
+
|
|
44
|
+
## 2. Signals
|
|
45
|
+
|
|
46
|
+
A **signal** is a reactive container for a value that can change over time.
|
|
47
|
+
|
|
48
|
+
In @tldraw/state, there are two types of signals:
|
|
49
|
+
|
|
50
|
+
- An **Atom** is the basic type of signal that acts as a container for a single value.
|
|
51
|
+
- A **Computed** is a signal that derives its value from several other signals (atoms or other computeds).
|
|
52
|
+
|
|
53
|
+
### Atoms: The State Containers
|
|
54
|
+
|
|
55
|
+
Atoms are the foundation of your application's state. They contain "raw" values that the rest of your application will use and react to.
|
|
56
|
+
|
|
57
|
+
#### Creating Atoms
|
|
58
|
+
|
|
59
|
+
You create an atom using the atom function. You must give it a name and an initial value.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { atom } from '@tldraw/state'
|
|
63
|
+
|
|
64
|
+
const count = atom('count', 0)
|
|
65
|
+
const user = atom('user', { name: 'Alice', age: 30 })
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> Tip: The name is used for debugging purposes, specifically for the `whyAmIRunning` function described later in these docs.
|
|
69
|
+
|
|
70
|
+
#### Reading an Atom's Value
|
|
71
|
+
|
|
72
|
+
To get the current value of an atom, use its `.get()` method.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
console.log(count.get()) // 0
|
|
76
|
+
console.log(user.get().name) // 'Alice'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> Tip: When you call `.get()` inside the function body of computed or a reaction, the library automatically **captures** that signal as a dependency.
|
|
80
|
+
|
|
81
|
+
#### Updating an Atom's Value
|
|
82
|
+
|
|
83
|
+
You can change an atom's value in two ways:
|
|
84
|
+
|
|
85
|
+
1. `.set(newValue)`: Directly sets the atom to a new value.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
count.set(1)
|
|
89
|
+
console.log(count.get()) // 1
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
2. `.update(updaterFn)`: Takes a function that receives the current value and returns the new value. This is useful when the new state depends on the old one.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
count.update((currentValue) => currentValue + 1)
|
|
96
|
+
console.log(count.get()) // 2
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
> Tip: If you try to set an atom to a value that is equal to its current value, the update will be skipped, and no reactions will be triggered.
|
|
100
|
+
|
|
101
|
+
#### Atom Options
|
|
102
|
+
|
|
103
|
+
You can pass an options object as the third argument to atom to customize its behavior.
|
|
104
|
+
|
|
105
|
+
- `isEqual`: A function to compare the old and new values to determine if the atom has changed. This is useful for complex objects.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// This atom will only update if the 'id' property changes.
|
|
109
|
+
const activeUser = atom('activeUser', { id: 1, name: 'Bob' }, { isEqual: (a, b) => a.id === b.id })
|
|
110
|
+
|
|
111
|
+
// This will NOT trigger an update because the IDs are the same.
|
|
112
|
+
activeUser.set({ id: 1, name: 'Robert' })
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- `historyLength` & `computeDiff`: These options are used for tracking changes over time. See the "History and Diffs" section for more details.
|
|
116
|
+
|
|
117
|
+
### Computeds: The Derived Values
|
|
118
|
+
|
|
119
|
+
A Computed is a signal whose value is derived from other signals. You can use computed signals to create complex data models that automatically stay in sync.
|
|
120
|
+
|
|
121
|
+
#### Creating Computeds
|
|
122
|
+
|
|
123
|
+
You create a computed signal using the `computed` function. It takes a name and a function that calculates its value. Inside this function, you can `.get()` the value of other signals.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { atom, computed } from '@tldraw/state'
|
|
127
|
+
|
|
128
|
+
const firstName = atom('firstName', 'John')
|
|
129
|
+
const lastName = atom('lastName', 'Doe')
|
|
130
|
+
|
|
131
|
+
const fullName = computed('fullName', (prevValue) => {
|
|
132
|
+
return `${firstName.get()} ${lastName.get()}`
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
console.log(fullName.get()) // "John Doe"
|
|
136
|
+
|
|
137
|
+
// Now, if we change a dependency...
|
|
138
|
+
firstName.set('Jane')
|
|
139
|
+
|
|
140
|
+
// ...the computed signal automatically updates!
|
|
141
|
+
console.log(fullName.get()) // "Jane Doe"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Note that computed signals capture both atoms and other computed signals as dependencies. Following the example above, if we create a new computed signal that depends on `fullName`, it will automatically update when `fullName` changes.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const greeting = computed('greeting', (prevValue) => {
|
|
148
|
+
return `Hello, ${fullName.get()}!`
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
firstName.set('Sam')
|
|
152
|
+
|
|
153
|
+
console.log(greeting.get()) // "Hello, Sam Doe!"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### Dependency Capture
|
|
157
|
+
|
|
158
|
+
This automatic dependency tracking works through a process called **dependency capture**. When the `fullName` function runs, the library actively "listens" for any calls to `.get()`. Each signal that is "gotten" is automatically registered as a dependency of `fullName`. The list of dependencies is updated every time the function re-runs, so they can even change dynamically.
|
|
159
|
+
|
|
160
|
+
#### Lazy Evaluation
|
|
161
|
+
|
|
162
|
+
Computed signals are evaluated **lazily**. The calculation function only runs when you call `.get()` on the computed _and_ one of its captured signal dependencies has changed since the last time it was gotten. If nothing has changed, the computed returns its previous cached value.
|
|
163
|
+
|
|
164
|
+
#### Using `@computed` as a Decorator
|
|
165
|
+
|
|
166
|
+
For classes, you can use the `@computed` decorator to create a computed property from a getter method. This is a clean way to co-locate derived data with its related state.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
class User {
|
|
170
|
+
firstName = atom('firstName', 'John')
|
|
171
|
+
lastName = atom('lastName', 'Doe')
|
|
172
|
+
|
|
173
|
+
@computed
|
|
174
|
+
getFullName() {
|
|
175
|
+
return `${this.firstName.get()} ${this.lastName.get()}`
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const user = new User()
|
|
180
|
+
console.log(user.getFullName()) // "John Doe"
|
|
181
|
+
|
|
182
|
+
user.firstName.set('Jane')
|
|
183
|
+
console.log(user.getFullName()) // "Jane Doe"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
If you need to access the underlying computed instance for a computed property created with the `@computed` decorator, you can use the `getComputedInstance` function.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
const user = new User()
|
|
190
|
+
const fullNameComputed = getComputedInstance(user, 'getFullName')
|
|
191
|
+
console.log(fullNameComputed.get()) // "John Doe"
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## 3. Reactivity and Side Effects
|
|
195
|
+
|
|
196
|
+
Reading and deriving state is only half the story. The other half is performing actions, called _side effects_, that run when state changes. Side effects can be used for anything: updating the DOM, logging to the console, making a network request, and so on.
|
|
197
|
+
|
|
198
|
+
### Simple Reactions with `react`
|
|
199
|
+
|
|
200
|
+
The easiest way to create a side effect is with the `react` function. You give it a name and a function to run. The library automatically **captures** which signals the function `.get()`s as dependencies and will re-run it whenever any of them change.
|
|
201
|
+
|
|
202
|
+
When created, `react` will immediate run your function once. It returns a `stop` function that you can call to tear down the reaction and stop it from listening to changes.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { atom, react } from '@tldraw/state'
|
|
206
|
+
|
|
207
|
+
const color = atom('color', 'red')
|
|
208
|
+
|
|
209
|
+
const stop = react('Update document title', () => {
|
|
210
|
+
document.title = `The color is ${color.get()}`
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// The title is now "The color is red"
|
|
214
|
+
|
|
215
|
+
color.set('blue')
|
|
216
|
+
// The title is now "The color is blue"
|
|
217
|
+
|
|
218
|
+
// To clean up the effect:
|
|
219
|
+
stop()
|
|
220
|
+
|
|
221
|
+
color.set('green')
|
|
222
|
+
// The title remains "The color is blue"
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
> Tip: The stop function is perfect for "fire-and-forget" effects, especially within UI components (e.g., in a useEffect hook in React).
|
|
226
|
+
|
|
227
|
+
### Controlled Reactions with `reactor`
|
|
228
|
+
|
|
229
|
+
For more control over the lifecycle of an effect, you can use `reactor`. It's similar to `react` but it doesn't start automatically. Instead, it returns a Reactor object with `.start()` and `.stop()` methods.
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { atom, reactor } from '@tldraw/state'
|
|
233
|
+
|
|
234
|
+
const name = atom('name', 'world')
|
|
235
|
+
|
|
236
|
+
const greeter = reactor('Greeter', () => {
|
|
237
|
+
console.log(`Hello, ${name.get()}!`)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// Nothing has been logged yet.
|
|
241
|
+
|
|
242
|
+
greeter.start()
|
|
243
|
+
// Logs: "Hello, world!"
|
|
244
|
+
|
|
245
|
+
name.set('galaxy')
|
|
246
|
+
// Logs: "Hello, galaxy!"
|
|
247
|
+
|
|
248
|
+
greeter.stop()
|
|
249
|
+
|
|
250
|
+
name.set('universe')
|
|
251
|
+
// Nothing is logged.
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
> Tip: A reactor is useful when you have a long-lived effect that needs to be paused and resumed based on application logic.
|
|
255
|
+
|
|
256
|
+
## 4. Advanced Topics
|
|
257
|
+
|
|
258
|
+
### Transactions: Batching State Updates
|
|
259
|
+
|
|
260
|
+
When you update multiple atoms that are dependencies of the same reaction, you might cause the reaction to re-run multiple times. transacts solve this by batching all state changes into a single, atomic update, after which reactions will execute.
|
|
261
|
+
|
|
262
|
+
#### Using transact()
|
|
263
|
+
|
|
264
|
+
The `transact` function takes a callback. All state updates inside this callback are queued. Reactions are only triggered _after_ the callback has finished executing successfully.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
const firstName = atom('firstName', 'John')
|
|
268
|
+
const lastName = atom('lastName', 'Doe')
|
|
269
|
+
|
|
270
|
+
react('greet', () => {
|
|
271
|
+
console.log(`Hello, ${firstName.get()} ${lastName.get()}!`)
|
|
272
|
+
})
|
|
273
|
+
// Logs: "Hello, John Doe!"
|
|
274
|
+
|
|
275
|
+
transact(() => {
|
|
276
|
+
// These two updates will be batched.
|
|
277
|
+
firstName.set('Jane')
|
|
278
|
+
lastName.set('Smith')
|
|
279
|
+
// The reaction has NOT run yet.
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
// NOW the reaction runs.
|
|
283
|
+
// Logs: "Hello, Jane Smith!"
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
#### Aborting and Rolling Back
|
|
287
|
+
|
|
288
|
+
Transactions may be aborted. Aborting a transaction will restore previous values of all signals modified inside of the transaction.
|
|
289
|
+
|
|
290
|
+
Rollbacks also occur automatically if an error is thrown inside the transaction.
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
const name = atom('name', 'Alice')
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
transact((rollback) => {
|
|
297
|
+
name.set('Bob')
|
|
298
|
+
throw new Error('Something went wrong')
|
|
299
|
+
})
|
|
300
|
+
} catch (e) {
|
|
301
|
+
// The transaction was aborted.
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
console.log(name.get()) // "Alice"
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
You can also abort a transaction manually by calling the `rollback` function, which is passed to the transaction callback.
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
const name = atom('name', 'Alice')
|
|
311
|
+
|
|
312
|
+
transact((rollback) => {
|
|
313
|
+
name.set('Bob')
|
|
314
|
+
rollback() // Discard the change
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
console.log(name.get()) // "Alice"
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Aborting a transaction will _only_ restore the values of the signals that were modified inside of the transaction. Other types of data or parts of your application will not be affected.
|
|
321
|
+
|
|
322
|
+
#### Nested Transactions
|
|
323
|
+
|
|
324
|
+
You can call `transact` inside of another transaction. A new transaction will only be created if there is not already one in progress.
|
|
325
|
+
|
|
326
|
+
If you want to create nested transactions (in order to take advantage of the rollback functionality), you can use the `transaction` function instead of `transact`.
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
transact(() => {
|
|
330
|
+
firstName.set('Jane')
|
|
331
|
+
|
|
332
|
+
transaction((rollback) => {
|
|
333
|
+
try {
|
|
334
|
+
lastName.set('Smith')
|
|
335
|
+
throw new Error('Something went wrong')
|
|
336
|
+
} catch (e) {
|
|
337
|
+
rollback()
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
console.log(firstName.get()) // "Jane"
|
|
343
|
+
console.log(lastName.get()) // "Doe" // The change was rolled back
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### History and Diffs
|
|
347
|
+
|
|
348
|
+
@tldraw/state can automatically track the history of changes to a signal, which is invaluable for features like undo/redo or creating sync engines.
|
|
349
|
+
|
|
350
|
+
To enable history, you must provide the `historyLength` option when creating an atom or computed.
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
const count = atom('count', 0, {
|
|
354
|
+
historyLength: 10,
|
|
355
|
+
// You can also provide a function to compute the difference (diff)
|
|
356
|
+
computeDiff: (a, b) => b - a,
|
|
357
|
+
})
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
The `historyLength` option defines the maximum number of diffs to keep in the history buffer. If you expect the atom to be part of an active effect subscription all the time, and to not change multiple times inside of a single transaction, you can set this to a relatively low number (e.g. 10). Otherwise, set this to a higher number based on your usage pattern and memory constraints.
|
|
361
|
+
|
|
362
|
+
#### Retrieving Diffs
|
|
363
|
+
|
|
364
|
+
Once history is enabled, you can use `getDiffSince(epoch)` to get an array of diffs that occurred since a specific point in time.
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
import { getGlobalEpoch } from '@tldraw/state'
|
|
368
|
+
|
|
369
|
+
const startEpoch = getGlobalEpoch()
|
|
370
|
+
|
|
371
|
+
count.set(5) // diff is 5
|
|
372
|
+
count.set(12) // diff is 7
|
|
373
|
+
|
|
374
|
+
const diffs = count.getDiffSince(startEpoch)
|
|
375
|
+
console.log(diffs) // [5, 7]
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
If the library doesn't have enough history to compute the diffs, it will return the special `RESET_VALUE` symbol. This tells you that you need to re-compute the state from scratch instead of applying patches.
|
|
379
|
+
|
|
380
|
+
### Computed Options
|
|
381
|
+
|
|
382
|
+
Similar to atoms, you can provide a `ComputedOptions` object as the second argument to the `computed` function.
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
const fullName = computed('fullName', () => {
|
|
386
|
+
return (`${firstName.get()} ${lastName.get()}`, { isEqual: (a, b) => a === b })
|
|
387
|
+
})
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
You also can pass in a `ComputedOptions` when used the `@computed` decorator.
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
class Counter {
|
|
394
|
+
max = 100
|
|
395
|
+
count = atom<number>(0)
|
|
396
|
+
|
|
397
|
+
@computed({ isEqual: (a, b) => a === b })
|
|
398
|
+
get remaining() {
|
|
399
|
+
return this.max - this.count.get()
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Incremental Computation
|
|
405
|
+
|
|
406
|
+
Computed signals can take advantage of diffs to compute a value incrementally.
|
|
407
|
+
|
|
408
|
+
In addition to the options described for atoms, you can also provide a `computeDiff` function. This function is used to compute the diff between the previous and new values of the computed signal.
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
const count = atom('count', 0)
|
|
412
|
+
const double = computed(
|
|
413
|
+
'double',
|
|
414
|
+
(prevValue) => {
|
|
415
|
+
return count.get() * 2
|
|
416
|
+
},
|
|
417
|
+
{ computeDiff: (a, b) => b - a }
|
|
418
|
+
)
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
You can use the `withDiff` helper to wrap the return value of a computed signal function, indicating that the diff should be used instead of calculating a new one with `AtomOptions.computeDiff`.
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
const count = atom('count', 0)
|
|
425
|
+
const double = computed('double', (prevValue) => {
|
|
426
|
+
const nextValue = count.get() * 2
|
|
427
|
+
if (isUninitialized(prevValue)) {
|
|
428
|
+
return nextValue
|
|
429
|
+
}
|
|
430
|
+
return withDiff(nextValue, nextValue - prevValue)
|
|
431
|
+
})
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
#### Handling the First Computed Run
|
|
435
|
+
|
|
436
|
+
Sometimes you need to know if a computed function is running for the very first time. The function is called with the previous value, which will be the special symbol `UNINITIALIZED` on the first run. You can check for this using the `isUninitialized` helper. This is particularly useful for incremental computations.
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
import { isUninitialized } from '@tldraw/state'
|
|
440
|
+
|
|
441
|
+
const list = computed('list', (prevValue) => {
|
|
442
|
+
if (isUninitialized(prevValue)) {
|
|
443
|
+
console.log('Calculating list for the first time!')
|
|
444
|
+
// ... perform expensive initial calculation
|
|
445
|
+
} else {
|
|
446
|
+
// ... perform cheaper incremental update
|
|
447
|
+
}
|
|
448
|
+
// ...
|
|
449
|
+
})
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Using the EffectScheduler
|
|
453
|
+
|
|
454
|
+
Under the hood, both `react` and `reactor` are powered by the `EffectScheduler`. You can use this low-level API for advanced use cases, like batching multiple effects together to run in the next animation frame.
|
|
455
|
+
|
|
456
|
+
You can provide a `scheduleEffect` function in the options. This function receives the execute callback, and it's up to you to decide when to call it.
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
let isRafScheduled = false
|
|
460
|
+
const scheduledEffects = []
|
|
461
|
+
|
|
462
|
+
const scheduleEffect = (execute) => {
|
|
463
|
+
scheduledEffects.push(execute)
|
|
464
|
+
if (!isRafScheduled) {
|
|
465
|
+
isRafScheduled = true
|
|
466
|
+
requestAnimationFrame(() => {
|
|
467
|
+
isRafScheduled = false
|
|
468
|
+
// Run all batched effects
|
|
469
|
+
scheduledEffects.forEach((fn) => fn())
|
|
470
|
+
scheduledEffects.length = 0
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const stop = react(
|
|
476
|
+
'Update DOM batched',
|
|
477
|
+
() => {
|
|
478
|
+
/* ... update the DOM ... */
|
|
479
|
+
},
|
|
480
|
+
{ scheduleEffect }
|
|
481
|
+
)
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Performance Optimization
|
|
485
|
+
|
|
486
|
+
While @tldraw/state is fast by default, there are tools for fine-tuning performance in demanding situations.
|
|
487
|
+
|
|
488
|
+
#### unsafe\_\_withoutCapture()
|
|
489
|
+
|
|
490
|
+
As explained earlier, when a computed or reaction runs, it automatically captures any signals you `.get()` as dependencies. Sometimes, however, you need to read a signal's value _without_ creating this dependency. `unsafe__withoutCapture` lets you step out of the current capture phase to do exactly that.
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
const frequentlyChangingValue = atom('frequent', 0)
|
|
494
|
+
const importantValue = atom('important', 'A')
|
|
495
|
+
|
|
496
|
+
react('log important changes', () => {
|
|
497
|
+
console.log(`Important value changed to ${importantValue.get()}`)
|
|
498
|
+
|
|
499
|
+
// We read this value, but don't create a dependency on it.
|
|
500
|
+
const otherValue = unsafe__withoutCapture(() => frequentlyChangingValue.get())
|
|
501
|
+
console.log(`(The other value was ${otherValue} at the time)`)
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
// This will NOT re-run the reaction.
|
|
505
|
+
frequentlyChangingValue.set(1)
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Type Guards and Utilities
|
|
509
|
+
|
|
510
|
+
The library exports several type guard functions to help you work with signals in TypeScript.
|
|
511
|
+
|
|
512
|
+
- `isSignal(value)`: Returns true if the value is an atom or a computed.
|
|
513
|
+
- `isAtom(value)`: Returns true if the value is an atom.
|
|
514
|
+
- `isComputed(value)`: Returns true if the value is a computed.
|
|
515
|
+
|
|
516
|
+
## 5. Debugging
|
|
517
|
+
|
|
518
|
+
Because @tldraw/state manages a graph of dependencies, it can sometimes be tricky to understand why a particular reaction or computed signal is re-running. The library provides a powerful utility to help with this.
|
|
519
|
+
|
|
520
|
+
### whyAmIRunning()
|
|
521
|
+
|
|
522
|
+
If you're ever confused about what caused an effect to run, you can call `whyAmIRunning()` at the beginning of its function. It will log a detailed, hierarchical tree to the console, showing you exactly which atom(s) changed and triggered the update.
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
import { atom, computed, react, whyAmIRunning } from '@tldraw/state'
|
|
526
|
+
|
|
527
|
+
const name = atom('name', 'Bob')
|
|
528
|
+
const age = atom('age', 42)
|
|
529
|
+
|
|
530
|
+
const greeting = computed('greeting', () => {
|
|
531
|
+
// We don't need to debug this one.
|
|
532
|
+
return `Hello, ${name.get()}`
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
react('log details', () => {
|
|
536
|
+
// But we want to know why this reaction runs.
|
|
537
|
+
whyAmIRunning()
|
|
538
|
+
|
|
539
|
+
console.log(`${greeting.get()} is ${age.get()} years old.`)
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
// On the first run, it logs:
|
|
543
|
+
// Effect(log details) was executed manually.
|
|
544
|
+
|
|
545
|
+
age.set(43)
|
|
546
|
+
|
|
547
|
+
// When age is updated, it logs:
|
|
548
|
+
// Effect(log details) is executing because:
|
|
549
|
+
// ↳ Atom(age) changed
|
|
550
|
+
|
|
551
|
+
name.set('Alice')
|
|
552
|
+
|
|
553
|
+
// When name is updated, it logs:
|
|
554
|
+
// Effect(log details) is executing because:
|
|
555
|
+
// ↳ Computed(greeting) changed
|
|
556
|
+
// ↳ Atom(name) changed
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
This makes it much easier to trace the flow of data and updates through your application.
|
|
560
|
+
|
|
561
|
+
## 6. Integrations with React
|
|
562
|
+
|
|
563
|
+
In addition to the core library, @tldraw/state provides a separate package, @tldraw/state-react, for integrating with the React framework. This library is documented separately.
|
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
`@tldraw/state` powers the reactive system at the heart of [tldraw](https://www.tldraw.com), handling everything from canvas updates to collaborative state synchronization. It's designed to work seamlessly with [@tldraw/store](https://github.com/tldraw/tldraw/tree/main/packages/store) and has optional [React bindings](https://github.com/tldraw/tldraw/tree/main/packages/state-react).
|
|
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
|
## Why @tldraw/state?
|
|
8
12
|
|
|
9
13
|
- **Fine-grained reactivity** - Only re-runs computations when their actual dependencies change
|
|
@@ -269,7 +273,7 @@ Looking for more examples? Check out:
|
|
|
269
273
|
|
|
270
274
|
## Contributing
|
|
271
275
|
|
|
272
|
-
|
|
276
|
+
Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new).
|
|
273
277
|
|
|
274
278
|
## License
|
|
275
279
|
|
package/dist-cjs/index.js
CHANGED
package/dist-esm/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/state",
|
|
3
3
|
"description": "tldraw infinite canvas SDK (state).",
|
|
4
|
-
"version": "5.2.0-next.
|
|
4
|
+
"version": "5.2.0-next.cd4a35fc06d5",
|
|
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",
|
|
@@ -45,10 +46,10 @@
|
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@types/lodash": "^4.17.14",
|
|
47
48
|
"lodash": "^4.17.21",
|
|
48
|
-
"vitest": "^
|
|
49
|
+
"vitest": "^4.1.7"
|
|
49
50
|
},
|
|
50
51
|
"dependencies": {
|
|
51
|
-
"@tldraw/utils": "5.2.0-next.
|
|
52
|
+
"@tldraw/utils": "5.2.0-next.cd4a35fc06d5"
|
|
52
53
|
},
|
|
53
54
|
"typedoc": {
|
|
54
55
|
"readmeFile": "none",
|