@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.
Files changed (71) hide show
  1. package/DOCS.md +64 -63
  2. package/README.md +35 -36
  3. package/dist-cjs/index.d.ts +26 -27
  4. package/dist-cjs/index.js +1 -1
  5. package/dist-cjs/lib/ArraySet.js +47 -144
  6. package/dist-cjs/lib/ArraySet.js.map +2 -2
  7. package/dist-cjs/lib/Atom.js +12 -26
  8. package/dist-cjs/lib/Atom.js.map +2 -2
  9. package/dist-cjs/lib/Computed.js +36 -64
  10. package/dist-cjs/lib/Computed.js.map +2 -2
  11. package/dist-cjs/lib/EffectScheduler.js +1 -1
  12. package/dist-cjs/lib/EffectScheduler.js.map +2 -2
  13. package/dist-cjs/lib/HistoryBuffer.js +8 -8
  14. package/dist-cjs/lib/HistoryBuffer.js.map +2 -2
  15. package/dist-cjs/lib/capture.js +1 -3
  16. package/dist-cjs/lib/capture.js.map +2 -2
  17. package/dist-cjs/lib/constants.js.map +2 -2
  18. package/dist-cjs/lib/helpers.js +3 -11
  19. package/dist-cjs/lib/helpers.js.map +2 -2
  20. package/dist-cjs/lib/localStorageAtom.js +7 -2
  21. package/dist-cjs/lib/localStorageAtom.js.map +2 -2
  22. package/dist-cjs/lib/transactions.js +11 -19
  23. package/dist-cjs/lib/transactions.js.map +2 -2
  24. package/dist-cjs/lib/types.js.map +1 -1
  25. package/dist-cjs/lib/warnings.js +2 -4
  26. package/dist-cjs/lib/warnings.js.map +2 -2
  27. package/dist-esm/index.d.mts +26 -27
  28. package/dist-esm/index.mjs +1 -1
  29. package/dist-esm/lib/ArraySet.mjs +47 -144
  30. package/dist-esm/lib/ArraySet.mjs.map +2 -2
  31. package/dist-esm/lib/Atom.mjs +12 -26
  32. package/dist-esm/lib/Atom.mjs.map +2 -2
  33. package/dist-esm/lib/Computed.mjs +36 -64
  34. package/dist-esm/lib/Computed.mjs.map +2 -2
  35. package/dist-esm/lib/EffectScheduler.mjs +1 -1
  36. package/dist-esm/lib/EffectScheduler.mjs.map +2 -2
  37. package/dist-esm/lib/HistoryBuffer.mjs +8 -8
  38. package/dist-esm/lib/HistoryBuffer.mjs.map +2 -2
  39. package/dist-esm/lib/capture.mjs +1 -3
  40. package/dist-esm/lib/capture.mjs.map +2 -2
  41. package/dist-esm/lib/constants.mjs.map +2 -2
  42. package/dist-esm/lib/helpers.mjs +3 -11
  43. package/dist-esm/lib/helpers.mjs.map +2 -2
  44. package/dist-esm/lib/localStorageAtom.mjs +7 -2
  45. package/dist-esm/lib/localStorageAtom.mjs.map +2 -2
  46. package/dist-esm/lib/transactions.mjs +11 -19
  47. package/dist-esm/lib/transactions.mjs.map +2 -2
  48. package/dist-esm/lib/types.mjs.map +1 -1
  49. package/dist-esm/lib/warnings.mjs +2 -4
  50. package/dist-esm/lib/warnings.mjs.map +2 -2
  51. package/package.json +2 -2
  52. package/src/lib/ArraySet.ts +68 -176
  53. package/src/lib/Atom.ts +27 -31
  54. package/src/lib/Computed.ts +68 -96
  55. package/src/lib/EffectScheduler.ts +9 -8
  56. package/src/lib/HistoryBuffer.ts +12 -10
  57. package/src/lib/__tests__/ArraySet.test.ts +39 -13
  58. package/src/lib/__tests__/EffectScheduler.test.ts +18 -0
  59. package/src/lib/__tests__/HistoryBuffer.test.ts +6 -3
  60. package/src/lib/__tests__/computed.test.ts +75 -0
  61. package/src/lib/__tests__/errors.test.ts +24 -0
  62. package/src/lib/__tests__/helpers.test.ts +7 -11
  63. package/src/lib/__tests__/history.test.ts +32 -2
  64. package/src/lib/__tests__/localStorageAtom.test.ts +15 -0
  65. package/src/lib/capture.ts +13 -13
  66. package/src/lib/constants.ts +3 -22
  67. package/src/lib/helpers.ts +15 -140
  68. package/src/lib/localStorageAtom.ts +9 -2
  69. package/src/lib/transactions.ts +23 -47
  70. package/src/lib/types.ts +7 -7
  71. package/src/lib/warnings.ts +2 -10
package/DOCS.md CHANGED
@@ -1,4 +1,4 @@
1
- # @tldraw/state Documentation
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 Example
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
- window.alert(greeting.get())
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 alerts the user when it changes.
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: The State Containers
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 Atoms
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 Atom's Value
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 Atom's Value
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 Options
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 Diffs" section for more details.
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: The Derived Values
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 Computeds
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 Capture
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 Evaluation
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 Decorator
164
+ #### Using `@computed` as a decorator
165
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.
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 Side Effects
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 Reactions with `react`
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 Reactions with `reactor`
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 Topics
256
+ ## 4. Advanced topics
257
257
 
258
- ### Transactions: Batching State Updates
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. transacts solve this by batching all state changes into a single, atomic update, after which reactions will execute.
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 successfully.
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 Rolling Back
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
- transact((rollback) => {
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 is passed to the transaction callback.
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
- transact((rollback) => {
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 Transactions
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 Diffs
346
+ ### History and diffs
347
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.
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 Diffs
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
- import { getGlobalEpoch } from '@tldraw/state'
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 Options
378
+ ### Computed options
381
379
 
382
- Similar to atoms, you can provide a `ComputedOptions` object as the second argument to the `computed` function.
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
- return (`${firstName.get()} ${lastName.get()}`, { isEqual: (a, b) => a === b })
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 used the `@computed` decorator.
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<number>(0)
393
+ count = atom('count', 0)
396
394
 
397
395
  @computed({ isEqual: (a, b) => a === b })
398
- get remaining() {
396
+ getRemaining() {
399
397
  return this.max - this.count.get()
400
398
  }
401
399
  }
402
400
  ```
403
401
 
404
- ### Incremental Computation
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 `AtomOptions.computeDiff`.
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('double', (prevValue) => {
426
- const nextValue = count.get() * 2
427
- if (isUninitialized(prevValue)) {
428
- return nextValue
429
- }
430
- return withDiff(nextValue, nextValue - prevValue)
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 First Computed Run
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 Optimization
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 Guards and Utilities
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
- - `isComputed(value)`: Returns true if the value is a computed.
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. It will log a detailed, hierarchical tree to the console, showing you exactly which atom(s) changed and triggered the update.
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
- // On the first run, it logs:
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
- // ↳ Computed(greeting) changed
556
- // ↳ Atom(name) changed
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
- - **Time travel** - Built-in history tracking and transactions with rollback support
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 Start
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 Concepts
60
+ ## Core concepts
61
61
 
62
- ### Atoms - State Containers
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 Values - Automatic Derivation
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 - Side Effects
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 - Batched Updates
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 Features
146
+ ## Advanced features
147
147
 
148
- ### History & Time Travel
148
+ ### History and diffs
149
149
 
150
- Track changes over time for undo/redo functionality:
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
- // Make changes...
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
- // Get diffs since a point in time
166
- const startTime = getGlobalEpoch()
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 Optimization
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 Examples
198
+ ## Integration examples
198
199
 
199
200
  ### With tldraw SDK
200
201
 
201
202
  ```ts
202
- // In a tldraw application
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
- function Counter() {
229
- const [count, setCount] = useAtom(countAtom)
230
- const doubled = useComputed(() => count * 2, [count])
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={() => setCount(count + 1)}>+</button>
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 Reference
241
+ ## API reference
243
242
 
244
243
  For complete API documentation, see [DOCS.md](./DOCS.md).
245
244
 
246
- ### Core Functions
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
- - `getGlobalEpoch()` - Get current time for history tracking
262
+ - `signal.getDiffSince(epoch)` - Get the diffs recorded since an epoch (see `signal.lastChangedEpoch`)
264
263
 
265
- ## Related Packages
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 & Patterns
271
+ ## Examples & patterns
273
272
 
274
273
  Looking for more examples? Check out:
275
274