@tldraw/state 5.3.2 → 5.4.0-canary.02cd0bd3b597
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 +64 -63
- package/README.md +35 -36
- package/dist-cjs/index.d.ts +26 -27
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/ArraySet.js +47 -144
- package/dist-cjs/lib/ArraySet.js.map +2 -2
- package/dist-cjs/lib/Atom.js +12 -26
- package/dist-cjs/lib/Atom.js.map +2 -2
- package/dist-cjs/lib/Computed.js +36 -64
- package/dist-cjs/lib/Computed.js.map +2 -2
- package/dist-cjs/lib/EffectScheduler.js +1 -1
- package/dist-cjs/lib/EffectScheduler.js.map +2 -2
- package/dist-cjs/lib/HistoryBuffer.js +8 -8
- package/dist-cjs/lib/HistoryBuffer.js.map +2 -2
- package/dist-cjs/lib/capture.js +1 -3
- package/dist-cjs/lib/capture.js.map +2 -2
- package/dist-cjs/lib/constants.js.map +2 -2
- package/dist-cjs/lib/helpers.js +3 -11
- package/dist-cjs/lib/helpers.js.map +2 -2
- package/dist-cjs/lib/localStorageAtom.js +7 -2
- package/dist-cjs/lib/localStorageAtom.js.map +2 -2
- package/dist-cjs/lib/transactions.js +11 -19
- package/dist-cjs/lib/transactions.js.map +2 -2
- package/dist-cjs/lib/types.js.map +1 -1
- package/dist-cjs/lib/warnings.js +2 -4
- package/dist-cjs/lib/warnings.js.map +2 -2
- package/dist-esm/index.d.mts +26 -27
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/ArraySet.mjs +47 -144
- package/dist-esm/lib/ArraySet.mjs.map +2 -2
- package/dist-esm/lib/Atom.mjs +12 -26
- package/dist-esm/lib/Atom.mjs.map +2 -2
- package/dist-esm/lib/Computed.mjs +36 -64
- package/dist-esm/lib/Computed.mjs.map +2 -2
- package/dist-esm/lib/EffectScheduler.mjs +1 -1
- package/dist-esm/lib/EffectScheduler.mjs.map +2 -2
- package/dist-esm/lib/HistoryBuffer.mjs +8 -8
- package/dist-esm/lib/HistoryBuffer.mjs.map +2 -2
- package/dist-esm/lib/capture.mjs +1 -3
- package/dist-esm/lib/capture.mjs.map +2 -2
- package/dist-esm/lib/constants.mjs.map +2 -2
- package/dist-esm/lib/helpers.mjs +3 -11
- package/dist-esm/lib/helpers.mjs.map +2 -2
- package/dist-esm/lib/localStorageAtom.mjs +7 -2
- package/dist-esm/lib/localStorageAtom.mjs.map +2 -2
- package/dist-esm/lib/transactions.mjs +11 -19
- package/dist-esm/lib/transactions.mjs.map +2 -2
- package/dist-esm/lib/types.mjs.map +1 -1
- package/dist-esm/lib/warnings.mjs +2 -4
- package/dist-esm/lib/warnings.mjs.map +2 -2
- package/package.json +2 -2
- package/src/lib/ArraySet.ts +68 -176
- package/src/lib/Atom.ts +27 -31
- package/src/lib/Computed.ts +68 -96
- package/src/lib/EffectScheduler.ts +9 -8
- package/src/lib/HistoryBuffer.ts +12 -10
- package/src/lib/__tests__/ArraySet.test.ts +39 -13
- package/src/lib/__tests__/EffectScheduler.test.ts +18 -0
- package/src/lib/__tests__/HistoryBuffer.test.ts +6 -3
- package/src/lib/__tests__/computed.test.ts +75 -0
- package/src/lib/__tests__/errors.test.ts +24 -0
- package/src/lib/__tests__/helpers.test.ts +7 -11
- package/src/lib/__tests__/history.test.ts +32 -2
- package/src/lib/__tests__/localStorageAtom.test.ts +15 -0
- package/src/lib/capture.ts +13 -13
- package/src/lib/constants.ts +3 -22
- package/src/lib/helpers.ts +15 -140
- package/src/lib/localStorageAtom.ts +9 -2
- package/src/lib/transactions.ts +23 -47
- package/src/lib/types.ts +7 -7
- package/src/lib/warnings.ts +2 -10
package/DOCS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @tldraw/state
|
|
1
|
+
# @tldraw/state documentation
|
|
2
2
|
|
|
3
3
|
## 1. Introduction
|
|
4
4
|
|
|
@@ -18,7 +18,7 @@ npm install @tldraw/state
|
|
|
18
18
|
|
|
19
19
|
@tldraw/state is written in TypeScript and provides excellent type safety out of the box. No additional types package needed.
|
|
20
20
|
|
|
21
|
-
### Quick
|
|
21
|
+
### Quick example
|
|
22
22
|
|
|
23
23
|
Here's a simple example to show how tldraw state works:
|
|
24
24
|
|
|
@@ -31,7 +31,7 @@ const greeting = computed('greeting', () => `Hello, ${name.get()}!`)
|
|
|
31
31
|
|
|
32
32
|
// React to changes
|
|
33
33
|
react('update page title', () => {
|
|
34
|
-
|
|
34
|
+
document.title = greeting.get()
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
// Update the state
|
|
@@ -39,7 +39,7 @@ name.set('tldraw state')
|
|
|
39
39
|
// Page title automatically updates to "Hello, tldraw state!"
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
In just a few lines, you've created reactive state that automatically
|
|
42
|
+
In just a few lines, you've created reactive state that automatically updates the page when it changes.
|
|
43
43
|
|
|
44
44
|
## 2. Signals
|
|
45
45
|
|
|
@@ -50,11 +50,11 @@ In @tldraw/state, there are two types of signals:
|
|
|
50
50
|
- An **Atom** is the basic type of signal that acts as a container for a single value.
|
|
51
51
|
- A **Computed** is a signal that derives its value from several other signals (atoms or other computeds).
|
|
52
52
|
|
|
53
|
-
### Atoms:
|
|
53
|
+
### Atoms: the state containers
|
|
54
54
|
|
|
55
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
56
|
|
|
57
|
-
#### Creating
|
|
57
|
+
#### Creating atoms
|
|
58
58
|
|
|
59
59
|
You create an atom using the atom function. You must give it a name and an initial value.
|
|
60
60
|
|
|
@@ -67,7 +67,7 @@ const user = atom('user', { name: 'Alice', age: 30 })
|
|
|
67
67
|
|
|
68
68
|
> Tip: The name is used for debugging purposes, specifically for the `whyAmIRunning` function described later in these docs.
|
|
69
69
|
|
|
70
|
-
#### Reading an
|
|
70
|
+
#### Reading an atom's value
|
|
71
71
|
|
|
72
72
|
To get the current value of an atom, use its `.get()` method.
|
|
73
73
|
|
|
@@ -78,7 +78,7 @@ console.log(user.get().name) // 'Alice'
|
|
|
78
78
|
|
|
79
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
80
|
|
|
81
|
-
#### Updating an
|
|
81
|
+
#### Updating an atom's value
|
|
82
82
|
|
|
83
83
|
You can change an atom's value in two ways:
|
|
84
84
|
|
|
@@ -98,7 +98,7 @@ console.log(count.get()) // 2
|
|
|
98
98
|
|
|
99
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
100
|
|
|
101
|
-
#### Atom
|
|
101
|
+
#### Atom options
|
|
102
102
|
|
|
103
103
|
You can pass an options object as the third argument to atom to customize its behavior.
|
|
104
104
|
|
|
@@ -112,13 +112,13 @@ const activeUser = atom('activeUser', { id: 1, name: 'Bob' }, { isEqual: (a, b)
|
|
|
112
112
|
activeUser.set({ id: 1, name: 'Robert' })
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
- `historyLength` & `computeDiff`: These options are used for tracking changes over time. See the "History and
|
|
115
|
+
- `historyLength` & `computeDiff`: These options are used for tracking changes over time. See the "History and diffs" section for more details.
|
|
116
116
|
|
|
117
|
-
### Computeds:
|
|
117
|
+
### Computeds: the derived values
|
|
118
118
|
|
|
119
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
120
|
|
|
121
|
-
#### Creating
|
|
121
|
+
#### Creating computeds
|
|
122
122
|
|
|
123
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
124
|
|
|
@@ -153,17 +153,17 @@ firstName.set('Sam')
|
|
|
153
153
|
console.log(greeting.get()) // "Hello, Sam Doe!"
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
-
#### Dependency
|
|
156
|
+
#### Dependency capture
|
|
157
157
|
|
|
158
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
159
|
|
|
160
|
-
#### Lazy
|
|
160
|
+
#### Lazy evaluation
|
|
161
161
|
|
|
162
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
163
|
|
|
164
|
-
#### Using `@computed` as a
|
|
164
|
+
#### Using `@computed` as a decorator
|
|
165
165
|
|
|
166
|
-
For classes, you can use the `@computed` decorator to create a computed property from a
|
|
166
|
+
For classes, you can use the `@computed` decorator to create a computed property from a method. This is a clean way to co-locate derived data with its related state.
|
|
167
167
|
|
|
168
168
|
```ts
|
|
169
169
|
class User {
|
|
@@ -191,11 +191,11 @@ const fullNameComputed = getComputedInstance(user, 'getFullName')
|
|
|
191
191
|
console.log(fullNameComputed.get()) // "John Doe"
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
-
## 3. Reactivity and
|
|
194
|
+
## 3. Reactivity and side effects
|
|
195
195
|
|
|
196
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
197
|
|
|
198
|
-
### Simple
|
|
198
|
+
### Simple reactions with `react`
|
|
199
199
|
|
|
200
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
201
|
|
|
@@ -224,7 +224,7 @@ color.set('green')
|
|
|
224
224
|
|
|
225
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
226
|
|
|
227
|
-
### Controlled
|
|
227
|
+
### Controlled reactions with `reactor`
|
|
228
228
|
|
|
229
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
230
|
|
|
@@ -253,15 +253,15 @@ name.set('universe')
|
|
|
253
253
|
|
|
254
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
255
|
|
|
256
|
-
## 4. Advanced
|
|
256
|
+
## 4. Advanced topics
|
|
257
257
|
|
|
258
|
-
### Transactions:
|
|
258
|
+
### Transactions: batching state updates
|
|
259
259
|
|
|
260
|
-
When you update multiple atoms that are dependencies of the same reaction, you might cause the reaction to re-run multiple times.
|
|
260
|
+
When you update multiple atoms that are dependencies of the same reaction, you might cause the reaction to re-run multiple times. Transactions solve this by batching all state changes into a single, atomic update, after which reactions will execute.
|
|
261
261
|
|
|
262
262
|
#### Using transact()
|
|
263
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
|
|
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 — once, whether the transaction commits or (see below) is rolled back.
|
|
265
265
|
|
|
266
266
|
```ts
|
|
267
267
|
const firstName = atom('firstName', 'John')
|
|
@@ -283,7 +283,7 @@ transact(() => {
|
|
|
283
283
|
// Logs: "Hello, Jane Smith!"
|
|
284
284
|
```
|
|
285
285
|
|
|
286
|
-
#### Aborting and
|
|
286
|
+
#### Aborting and rolling back
|
|
287
287
|
|
|
288
288
|
Transactions may be aborted. Aborting a transaction will restore previous values of all signals modified inside of the transaction.
|
|
289
289
|
|
|
@@ -293,7 +293,7 @@ Rollbacks also occur automatically if an error is thrown inside the transaction.
|
|
|
293
293
|
const name = atom('name', 'Alice')
|
|
294
294
|
|
|
295
295
|
try {
|
|
296
|
-
|
|
296
|
+
transaction(() => {
|
|
297
297
|
name.set('Bob')
|
|
298
298
|
throw new Error('Something went wrong')
|
|
299
299
|
})
|
|
@@ -304,12 +304,12 @@ try {
|
|
|
304
304
|
console.log(name.get()) // "Alice"
|
|
305
305
|
```
|
|
306
306
|
|
|
307
|
-
You can also abort a transaction manually by calling the `rollback` function, which
|
|
307
|
+
You can also abort a transaction manually by calling the `rollback` function, which `transaction` (but not `transact`) passes to its callback.
|
|
308
308
|
|
|
309
309
|
```ts
|
|
310
310
|
const name = atom('name', 'Alice')
|
|
311
311
|
|
|
312
|
-
|
|
312
|
+
transaction((rollback) => {
|
|
313
313
|
name.set('Bob')
|
|
314
314
|
rollback() // Discard the change
|
|
315
315
|
})
|
|
@@ -317,9 +317,9 @@ transact((rollback) => {
|
|
|
317
317
|
console.log(name.get()) // "Alice"
|
|
318
318
|
```
|
|
319
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.
|
|
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. Reactions that depend on those signals still run once after the rollback, observing the restored values.
|
|
321
321
|
|
|
322
|
-
#### Nested
|
|
322
|
+
#### Nested transactions
|
|
323
323
|
|
|
324
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
325
|
|
|
@@ -343,9 +343,9 @@ console.log(firstName.get()) // "Jane"
|
|
|
343
343
|
console.log(lastName.get()) // "Doe" // The change was rolled back
|
|
344
344
|
```
|
|
345
345
|
|
|
346
|
-
### History and
|
|
346
|
+
### History and diffs
|
|
347
347
|
|
|
348
|
-
@tldraw/state can automatically track the history of changes to a signal, which
|
|
348
|
+
@tldraw/state can automatically track the history of changes to a signal, which lets dependents update incrementally instead of recomputing from scratch.
|
|
349
349
|
|
|
350
350
|
To enable history, you must provide the `historyLength` option when creating an atom or computed.
|
|
351
351
|
|
|
@@ -359,14 +359,12 @@ const count = atom('count', 0, {
|
|
|
359
359
|
|
|
360
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
361
|
|
|
362
|
-
#### Retrieving
|
|
362
|
+
#### Retrieving diffs
|
|
363
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.
|
|
364
|
+
Once history is enabled, you can use `getDiffSince(epoch)` to get an array of diffs that occurred since a specific point in time. Every signal exposes `lastChangedEpoch`, the epoch at which it last changed, which is the natural point to measure from.
|
|
365
365
|
|
|
366
366
|
```ts
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const startEpoch = getGlobalEpoch()
|
|
367
|
+
const startEpoch = count.lastChangedEpoch
|
|
370
368
|
|
|
371
369
|
count.set(5) // diff is 5
|
|
372
370
|
count.set(12) // diff is 7
|
|
@@ -377,35 +375,35 @@ console.log(diffs) // [5, 7]
|
|
|
377
375
|
|
|
378
376
|
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
377
|
|
|
380
|
-
### Computed
|
|
378
|
+
### Computed options
|
|
381
379
|
|
|
382
|
-
Similar to atoms, you can provide a `ComputedOptions` object as the
|
|
380
|
+
Similar to atoms, you can provide a `ComputedOptions` object as the third argument to the `computed` function.
|
|
383
381
|
|
|
384
382
|
```ts
|
|
385
|
-
const fullName = computed('fullName', () => {
|
|
386
|
-
|
|
383
|
+
const fullName = computed('fullName', () => `${firstName.get()} ${lastName.get()}`, {
|
|
384
|
+
isEqual: (a, b) => a === b,
|
|
387
385
|
})
|
|
388
386
|
```
|
|
389
387
|
|
|
390
|
-
You also can pass in a `ComputedOptions` when
|
|
388
|
+
You also can pass in a `ComputedOptions` when using the `@computed` decorator.
|
|
391
389
|
|
|
392
390
|
```ts
|
|
393
391
|
class Counter {
|
|
394
392
|
max = 100
|
|
395
|
-
count = atom
|
|
393
|
+
count = atom('count', 0)
|
|
396
394
|
|
|
397
395
|
@computed({ isEqual: (a, b) => a === b })
|
|
398
|
-
|
|
396
|
+
getRemaining() {
|
|
399
397
|
return this.max - this.count.get()
|
|
400
398
|
}
|
|
401
399
|
}
|
|
402
400
|
```
|
|
403
401
|
|
|
404
|
-
### Incremental
|
|
402
|
+
### Incremental computation
|
|
405
403
|
|
|
406
404
|
Computed signals can take advantage of diffs to compute a value incrementally.
|
|
407
405
|
|
|
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.
|
|
406
|
+
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. As with atoms, diffs are only recorded when `historyLength` is set.
|
|
409
407
|
|
|
410
408
|
```ts
|
|
411
409
|
const count = atom('count', 0)
|
|
@@ -414,24 +412,28 @@ const double = computed(
|
|
|
414
412
|
(prevValue) => {
|
|
415
413
|
return count.get() * 2
|
|
416
414
|
},
|
|
417
|
-
{ computeDiff: (a, b) => b - a }
|
|
415
|
+
{ historyLength: 10, computeDiff: (a, b) => b - a }
|
|
418
416
|
)
|
|
419
417
|
```
|
|
420
418
|
|
|
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 `
|
|
419
|
+
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 `ComputedOptions.computeDiff`.
|
|
422
420
|
|
|
423
421
|
```ts
|
|
424
422
|
const count = atom('count', 0)
|
|
425
|
-
const double = computed(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
423
|
+
const double = computed(
|
|
424
|
+
'double',
|
|
425
|
+
(prevValue) => {
|
|
426
|
+
const nextValue = count.get() * 2
|
|
427
|
+
if (isUninitialized(prevValue)) {
|
|
428
|
+
return nextValue
|
|
429
|
+
}
|
|
430
|
+
return withDiff(nextValue, nextValue - prevValue)
|
|
431
|
+
},
|
|
432
|
+
{ historyLength: 10 }
|
|
433
|
+
)
|
|
432
434
|
```
|
|
433
435
|
|
|
434
|
-
#### Handling the
|
|
436
|
+
#### Handling the first computed run
|
|
435
437
|
|
|
436
438
|
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
439
|
|
|
@@ -481,7 +483,7 @@ const stop = react(
|
|
|
481
483
|
)
|
|
482
484
|
```
|
|
483
485
|
|
|
484
|
-
### Performance
|
|
486
|
+
### Performance optimization
|
|
485
487
|
|
|
486
488
|
While @tldraw/state is fast by default, there are tools for fine-tuning performance in demanding situations.
|
|
487
489
|
|
|
@@ -505,13 +507,13 @@ react('log important changes', () => {
|
|
|
505
507
|
frequentlyChangingValue.set(1)
|
|
506
508
|
```
|
|
507
509
|
|
|
508
|
-
### Type
|
|
510
|
+
### Type guards and utilities
|
|
509
511
|
|
|
510
512
|
The library exports several type guard functions to help you work with signals in TypeScript.
|
|
511
513
|
|
|
512
514
|
- `isSignal(value)`: Returns true if the value is an atom or a computed.
|
|
513
515
|
- `isAtom(value)`: Returns true if the value is an atom.
|
|
514
|
-
- `
|
|
516
|
+
- `isUninitialized(value)`: Returns true if the value is the `UNINITIALIZED` symbol passed to a computed function on its first run.
|
|
515
517
|
|
|
516
518
|
## 5. Debugging
|
|
517
519
|
|
|
@@ -519,7 +521,7 @@ Because @tldraw/state manages a graph of dependencies, it can sometimes be trick
|
|
|
519
521
|
|
|
520
522
|
### whyAmIRunning()
|
|
521
523
|
|
|
522
|
-
If you're ever confused about what caused an effect to run, you can call `whyAmIRunning()` at the beginning of its function.
|
|
524
|
+
If you're ever confused about what caused an effect to run, you can call `whyAmIRunning()` at the beginning of its function. From the next run on, it will log a detailed, hierarchical tree to the console, showing you exactly which atom(s) changed and triggered the update.
|
|
523
525
|
|
|
524
526
|
```ts
|
|
525
527
|
import { atom, computed, react, whyAmIRunning } from '@tldraw/state'
|
|
@@ -539,8 +541,7 @@ react('log details', () => {
|
|
|
539
541
|
console.log(`${greeting.get()} is ${age.get()} years old.`)
|
|
540
542
|
})
|
|
541
543
|
|
|
542
|
-
//
|
|
543
|
-
// Effect(log details) was executed manually.
|
|
544
|
+
// Nothing is logged on the first run (the run that arms whyAmIRunning).
|
|
544
545
|
|
|
545
546
|
age.set(43)
|
|
546
547
|
|
|
@@ -552,8 +553,8 @@ name.set('Alice')
|
|
|
552
553
|
|
|
553
554
|
// When name is updated, it logs:
|
|
554
555
|
// Effect(log details) is executing because:
|
|
555
|
-
//
|
|
556
|
-
//
|
|
556
|
+
// ↳ Computed(greeting) changed
|
|
557
|
+
// ↳ Atom(name) changed
|
|
557
558
|
```
|
|
558
559
|
|
|
559
560
|
This makes it much easier to trace the flow of data and updates through your application.
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ A `DOCS.md` file is included alongside this README in the published package, wit
|
|
|
17
17
|
- **Fine-grained reactivity** - Only re-runs computations when their actual dependencies change
|
|
18
18
|
- **High performance** - Lazy evaluation and efficient dependency tracking
|
|
19
19
|
- **Automatic updates** - Derived values and side effects update automatically
|
|
20
|
-
- **
|
|
20
|
+
- **Incremental updates** - Built-in diff history and transactions with rollback support
|
|
21
21
|
- **Framework agnostic** - Works with any JavaScript framework or vanilla JS
|
|
22
22
|
- **TypeScript first** - Excellent type safety with full TypeScript support
|
|
23
23
|
|
|
@@ -29,7 +29,7 @@ Perfect for building reactive UIs, real-time collaborative apps, and complex sta
|
|
|
29
29
|
npm install @tldraw/state
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
## Quick
|
|
32
|
+
## Quick start
|
|
33
33
|
|
|
34
34
|
```ts
|
|
35
35
|
import { atom, computed, react } from '@tldraw/state'
|
|
@@ -57,9 +57,9 @@ count.set(42)
|
|
|
57
57
|
// Logs: "Hello, tldraw! Count: 42"
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
## Core
|
|
60
|
+
## Core concepts
|
|
61
61
|
|
|
62
|
-
### Atoms -
|
|
62
|
+
### Atoms - state containers
|
|
63
63
|
|
|
64
64
|
Atoms hold raw values and are the foundation of your reactive state:
|
|
65
65
|
|
|
@@ -78,7 +78,7 @@ user.update((current) => ({ ...current, age: 31 }))
|
|
|
78
78
|
theme.set('dark')
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
### Computed
|
|
81
|
+
### Computed values - automatic derivation
|
|
82
82
|
|
|
83
83
|
Computed signals derive their values from other signals and update automatically:
|
|
84
84
|
|
|
@@ -98,7 +98,7 @@ firstName.set('Jane')
|
|
|
98
98
|
console.log(fullName.get()) // "Jane Doe" - automatically updated!
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
### Reactions -
|
|
101
|
+
### Reactions - side effects
|
|
102
102
|
|
|
103
103
|
Reactions run side effects when their dependencies change:
|
|
104
104
|
|
|
@@ -120,7 +120,7 @@ selectedId.set('shape-123')
|
|
|
120
120
|
stop()
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
### Transactions -
|
|
123
|
+
### Transactions - batched updates
|
|
124
124
|
|
|
125
125
|
Batch multiple updates to prevent intermediate reactions:
|
|
126
126
|
|
|
@@ -143,11 +143,11 @@ transact(() => {
|
|
|
143
143
|
// Logs: "(10, 20)"
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
## Advanced
|
|
146
|
+
## Advanced features
|
|
147
147
|
|
|
148
|
-
### History
|
|
148
|
+
### History and diffs
|
|
149
149
|
|
|
150
|
-
Track changes over time
|
|
150
|
+
Track changes over time so that dependents can update incrementally instead of recomputing from scratch:
|
|
151
151
|
|
|
152
152
|
```ts
|
|
153
153
|
const canvas = atom(
|
|
@@ -159,16 +159,17 @@ const canvas = atom(
|
|
|
159
159
|
}
|
|
160
160
|
)
|
|
161
161
|
|
|
162
|
-
//
|
|
162
|
+
// Remember where you are...
|
|
163
|
+
const startEpoch = canvas.lastChangedEpoch
|
|
164
|
+
|
|
165
|
+
// ... make changes ...
|
|
163
166
|
canvas.update((state) => ({ shapes: [...state.shapes, newShape] }))
|
|
164
167
|
|
|
165
|
-
//
|
|
166
|
-
const
|
|
167
|
-
// ... make more changes ...
|
|
168
|
-
const diffs = canvas.getDiffSince(startTime)
|
|
168
|
+
// ... and get the diffs since then (or RESET_VALUE if the history doesn't reach back that far)
|
|
169
|
+
const diffs = canvas.getDiffSince(startEpoch)
|
|
169
170
|
```
|
|
170
171
|
|
|
171
|
-
### Performance
|
|
172
|
+
### Performance optimization
|
|
172
173
|
|
|
173
174
|
Use `unsafe__withoutCapture` to read values without creating dependencies:
|
|
174
175
|
|
|
@@ -194,21 +195,18 @@ react('debug-reaction', () => {
|
|
|
194
195
|
})
|
|
195
196
|
```
|
|
196
197
|
|
|
197
|
-
## Integration
|
|
198
|
+
## Integration examples
|
|
198
199
|
|
|
199
200
|
### With tldraw SDK
|
|
200
201
|
|
|
201
202
|
```ts
|
|
202
|
-
//
|
|
203
|
-
const editor = useEditor()
|
|
204
|
-
|
|
205
|
-
// Create reactive state that works with tldraw
|
|
203
|
+
// e.g. in Tldraw's onMount callback, which receives the editor
|
|
206
204
|
const selectedShapes = computed('selectedShapes', () => {
|
|
207
205
|
return editor.getSelectedShapeIds().map((id) => editor.getShape(id))
|
|
208
206
|
})
|
|
209
207
|
|
|
210
|
-
// React to selection changes
|
|
211
|
-
react('update-property-panel', () => {
|
|
208
|
+
// React to selection changes; call the returned function to stop
|
|
209
|
+
const stop = react('update-property-panel', () => {
|
|
212
210
|
const shapes = selectedShapes.get()
|
|
213
211
|
updatePropertyPanel(shapes)
|
|
214
212
|
})
|
|
@@ -223,27 +221,28 @@ npm install @tldraw/state-react
|
|
|
223
221
|
```
|
|
224
222
|
|
|
225
223
|
```tsx
|
|
226
|
-
import { useAtom, useComputed } from '@tldraw/state-react'
|
|
224
|
+
import { track, useAtom, useComputed } from '@tldraw/state-react'
|
|
227
225
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
226
|
+
// track() re-renders the component when any signal it reads changes
|
|
227
|
+
const Counter = track(function Counter() {
|
|
228
|
+
const count = useAtom('count', 0)
|
|
229
|
+
const doubled = useComputed('doubled', () => count.get() * 2, [count])
|
|
231
230
|
|
|
232
231
|
return (
|
|
233
232
|
<div>
|
|
234
|
-
<p>Count: {count}</p>
|
|
235
|
-
<p>Doubled: {doubled}</p>
|
|
236
|
-
<button onClick={() =>
|
|
233
|
+
<p>Count: {count.get()}</p>
|
|
234
|
+
<p>Doubled: {doubled.get()}</p>
|
|
235
|
+
<button onClick={() => count.set(count.get() + 1)}>+</button>
|
|
237
236
|
</div>
|
|
238
237
|
)
|
|
239
|
-
}
|
|
238
|
+
})
|
|
240
239
|
```
|
|
241
240
|
|
|
242
|
-
## API
|
|
241
|
+
## API reference
|
|
243
242
|
|
|
244
243
|
For complete API documentation, see [DOCS.md](./DOCS.md).
|
|
245
244
|
|
|
246
|
-
### Core
|
|
245
|
+
### Core functions
|
|
247
246
|
|
|
248
247
|
- `atom(name, initialValue, options?)` - Create a reactive state container
|
|
249
248
|
- `computed(name, computeFn, options?)` - Create a derived value
|
|
@@ -260,16 +259,16 @@ For complete API documentation, see [DOCS.md](./DOCS.md).
|
|
|
260
259
|
- `unsafe__withoutCapture(fn)` - Read state without creating dependencies
|
|
261
260
|
- `whyAmIRunning()` - Debug what triggered an update
|
|
262
261
|
- `getComputedInstance(obj, prop)` - Get underlying computed instance
|
|
263
|
-
- `
|
|
262
|
+
- `signal.getDiffSince(epoch)` - Get the diffs recorded since an epoch (see `signal.lastChangedEpoch`)
|
|
264
263
|
|
|
265
|
-
## Related
|
|
264
|
+
## Related packages
|
|
266
265
|
|
|
267
266
|
- **[@tldraw/state-react](../state-react)** - React bindings for @tldraw/state
|
|
268
267
|
- **[@tldraw/store](../store)** - Record storage built on @tldraw/state
|
|
269
268
|
- **[@tldraw/editor](../editor)** - The tldraw canvas editor
|
|
270
269
|
- **[@tldraw/tldraw](../tldraw)** - Complete tldraw UI components
|
|
271
270
|
|
|
272
|
-
## Examples &
|
|
271
|
+
## Examples & patterns
|
|
273
272
|
|
|
274
273
|
Looking for more examples? Check out:
|
|
275
274
|
|