@tldraw/state
Advanced tools
Comparing version
@@ -90,3 +90,3 @@ /* Excluded from this release type: ArraySet */ | ||
* @param b - The new value | ||
* @returns | ||
* @returns True if the values are equal, false otherwise. | ||
*/ | ||
@@ -205,11 +205,11 @@ isEqual?(a: any, b: any): boolean; | ||
/** | ||
* A method used to compute a diff between the atom's old and new values. If provided, it will not be used unless you also specify {@link AtomOptions.historyLength}. | ||
* A method used to compute a diff between the computed's old and new values. If provided, it will not be used unless you also specify {@link ComputedOptions.historyLength}. | ||
*/ | ||
computeDiff?: ComputeDiff<Value, Diff>; | ||
/** | ||
* If provided, this will be used to compare the old and new values of the atom to determine if the value has changed. | ||
* If provided, this will be used to compare the old and new values of the computed to determine if the value has changed. | ||
* By default, values are compared using first using strict equality (`===`), then `Object.is`, and finally any `.equals` method present in the object's prototype chain. | ||
* @param a - The old value | ||
* @param b - The new value | ||
* @returns | ||
* @returns True if the values are equal, false otherwise. | ||
*/ | ||
@@ -242,3 +242,2 @@ isEqual?(a: any, b: any): boolean; | ||
export declare interface EffectScheduler<Result> { | ||
/** @internal */ | ||
/** | ||
@@ -274,2 +273,3 @@ * Whether this scheduler is attached and actively listening to its parents. | ||
* It will no longer be eligible to receive 'maybeScheduleEffect' calls until `EffectScheduler.attach` is called again. | ||
* @public | ||
*/ | ||
@@ -280,2 +280,3 @@ detach(): void; | ||
* @returns The result of the effect. | ||
* @public | ||
*/ | ||
@@ -314,3 +315,3 @@ execute(): Result; | ||
* @param execute - A function that will execute the effect. | ||
* @returns | ||
* @returns void | ||
*/ | ||
@@ -317,0 +318,0 @@ scheduleEffect?: (execute: () => void) => void; |
@@ -62,5 +62,5 @@ "use strict"; | ||
"@tldraw/state", | ||
"3.16.0-canary.dfdf6b7de8c2", | ||
"3.16.0-canary.e9c30b532b82", | ||
"cjs" | ||
); | ||
//# sourceMappingURL=index.js.map |
@@ -37,2 +37,3 @@ "use strict"; | ||
} | ||
/** @internal */ | ||
_isActivelyListening = false; | ||
@@ -49,4 +50,7 @@ /** | ||
lastTraversedEpoch = import_constants.GLOBAL_START_EPOCH; | ||
/** @internal */ | ||
lastReactedEpoch = import_constants.GLOBAL_START_EPOCH; | ||
/** @internal */ | ||
_scheduleCount = 0; | ||
/** @internal */ | ||
__debug_ancestor_epochs__ = null; | ||
@@ -67,2 +71,3 @@ /** | ||
parents = []; | ||
/** @internal */ | ||
_scheduleEffect; | ||
@@ -109,2 +114,3 @@ /** @internal */ | ||
* It will no longer be eligible to receive 'maybeScheduleEffect' calls until `EffectScheduler.attach` is called again. | ||
* @public | ||
*/ | ||
@@ -120,2 +126,3 @@ detach() { | ||
* @returns The result of the effect. | ||
* @public | ||
*/ | ||
@@ -122,0 +129,0 @@ execute() { |
{ | ||
"name": "@tldraw/state", | ||
"description": "tldraw infinite canvas SDK (state).", | ||
"version": "3.16.0-canary.dfdf6b7de8c2", | ||
"version": "3.16.0-canary.e9c30b532b82", | ||
"author": { | ||
@@ -59,3 +59,3 @@ "name": "tldraw Inc.", | ||
"dependencies": { | ||
"@tldraw/utils": "3.16.0-canary.dfdf6b7de8c2" | ||
"@tldraw/utils": "3.16.0-canary.e9c30b532b82" | ||
}, | ||
@@ -62,0 +62,0 @@ "typedoc": { |
273
README.md
# @tldraw/state | ||
A signals library for tldraw. See also the [React bindings](https://github.com/tldraw/tldraw/tree/main/packages/state-react) for this library. | ||
`@tldraw/state` is a powerful and lightweight TypeScript library for managing reactive state using signals. It provides fine-grained reactive primitives that automatically track dependencies and efficiently update only what needs to change. | ||
To learn more about this check out the [Signia library](https://signia.tldraw.dev/docs/using-signals) which provides the foundational concepts. | ||
`@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). | ||
## Contribution | ||
## Why @tldraw/state? | ||
- **Fine-grained reactivity** - Only re-runs computations when their actual dependencies change | ||
- **High performance** - Lazy evaluation and efficient dependency tracking | ||
- **Automatic updates** - Derived values and side effects update automatically | ||
- **Time travel** - Built-in history tracking and transactions with rollback support | ||
- **Framework agnostic** - Works with any JavaScript framework or vanilla JS | ||
- **TypeScript first** - Excellent type safety with full TypeScript support | ||
Perfect for building reactive UIs, real-time collaborative apps, and complex state machines where performance and predictability matter. | ||
## Installation | ||
```bash | ||
npm install @tldraw/state | ||
``` | ||
## Quick Start | ||
```ts | ||
import { atom, computed, react } from '@tldraw/state' | ||
// Create reactive state | ||
const name = atom('name', 'World') | ||
const count = atom('count', 0) | ||
// Derive values automatically | ||
const greeting = computed('greeting', () => { | ||
return `Hello, ${name.get()}! Count: ${count.get()}` | ||
}) | ||
// React to changes | ||
react('logger', () => { | ||
console.log(greeting.get()) | ||
}) | ||
// Logs: "Hello, World! Count: 0" | ||
// Update state - reactions run automatically | ||
name.set('tldraw') | ||
// Logs: "Hello, tldraw! Count: 0" | ||
count.set(42) | ||
// Logs: "Hello, tldraw! Count: 42" | ||
``` | ||
## Core Concepts | ||
### Atoms - State Containers | ||
Atoms hold raw values and are the foundation of your reactive state: | ||
```ts | ||
import { atom } from '@tldraw/state' | ||
// Create atoms with initial values | ||
const user = atom('user', { name: 'Alice', age: 30 }) | ||
const theme = atom('theme', 'light') | ||
// Read values | ||
console.log(user.get().name) // 'Alice' | ||
// Update values | ||
user.update((current) => ({ ...current, age: 31 })) | ||
theme.set('dark') | ||
``` | ||
### Computed Values - Automatic Derivation | ||
Computed signals derive their values from other signals and update automatically: | ||
```ts | ||
import { computed } from '@tldraw/state' | ||
const firstName = atom('firstName', 'John') | ||
const lastName = atom('lastName', 'Doe') | ||
const fullName = computed('fullName', () => { | ||
return `${firstName.get()} ${lastName.get()}` | ||
}) | ||
console.log(fullName.get()) // "John Doe" | ||
firstName.set('Jane') | ||
console.log(fullName.get()) // "Jane Doe" - automatically updated! | ||
``` | ||
### Reactions - Side Effects | ||
Reactions run side effects when their dependencies change: | ||
```ts | ||
import { react } from '@tldraw/state' | ||
const selectedId = atom('selectedId', null) | ||
// Update UI when selection changes | ||
const stop = react('update-selection-ui', () => { | ||
const id = selectedId.get() | ||
document.getElementById('selected').textContent = id || 'None' | ||
}) | ||
selectedId.set('shape-123') | ||
// UI automatically updates | ||
// Clean up when no longer needed | ||
stop() | ||
``` | ||
### Transactions - Batched Updates | ||
Batch multiple updates to prevent intermediate reactions: | ||
```ts | ||
import { transact } from '@tldraw/state' | ||
const x = atom('x', 0) | ||
const y = atom('y', 0) | ||
const position = computed('position', () => `(${x.get()}, ${y.get()})`) | ||
react('log-position', () => console.log(position.get())) | ||
// Logs: "(0, 0)" | ||
transact(() => { | ||
x.set(10) | ||
y.set(20) | ||
// Reaction runs only once after transaction | ||
}) | ||
// Logs: "(10, 20)" | ||
``` | ||
## Advanced Features | ||
### History & Time Travel | ||
Track changes over time for undo/redo functionality: | ||
```ts | ||
const canvas = atom( | ||
'canvas', | ||
{ shapes: [] }, | ||
{ | ||
historyLength: 100, | ||
computeDiff: (prev, next) => ({ prev, next }), | ||
} | ||
) | ||
// Make changes... | ||
canvas.update((state) => ({ shapes: [...state.shapes, newShape] })) | ||
// Get diffs since a point in time | ||
const startTime = getGlobalEpoch() | ||
// ... make more changes ... | ||
const diffs = canvas.getDiffSince(startTime) | ||
``` | ||
### Performance Optimization | ||
Use `unsafe__withoutCapture` to read values without creating dependencies: | ||
```ts | ||
const expensiveComputed = computed('expensive', () => { | ||
const important = importantValue.get() | ||
// Read this without making it a dependency | ||
const metadata = unsafe__withoutCapture(() => metadataAtom.get()) | ||
return computeExpensiveValue(important, metadata) | ||
}) | ||
``` | ||
### Debugging | ||
Use `whyAmIRunning()` to understand what triggered an update: | ||
```ts | ||
react('debug-reaction', () => { | ||
whyAmIRunning() // Logs dependency tree to console | ||
// Your reaction code... | ||
}) | ||
``` | ||
## Integration Examples | ||
### With tldraw SDK | ||
```ts | ||
// In a tldraw application | ||
const editor = useEditor() | ||
// Create reactive state that works with tldraw | ||
const selectedShapes = computed('selectedShapes', () => { | ||
return editor.getSelectedShapeIds().map((id) => editor.getShape(id)) | ||
}) | ||
// React to selection changes | ||
react('update-property-panel', () => { | ||
const shapes = selectedShapes.get() | ||
updatePropertyPanel(shapes) | ||
}) | ||
``` | ||
### With React | ||
Install the React bindings: | ||
```bash | ||
npm install @tldraw/state-react | ||
``` | ||
```tsx | ||
import { useAtom, useComputed } from '@tldraw/state-react' | ||
function Counter() { | ||
const [count, setCount] = useAtom(countAtom) | ||
const doubled = useComputed(() => count * 2, [count]) | ||
return ( | ||
<div> | ||
<p>Count: {count}</p> | ||
<p>Doubled: {doubled}</p> | ||
<button onClick={() => setCount(count + 1)}>+</button> | ||
</div> | ||
) | ||
} | ||
``` | ||
## API Reference | ||
For complete API documentation, see [DOCS.md](./DOCS.md). | ||
### Core Functions | ||
- `atom(name, initialValue, options?)` - Create a reactive state container | ||
- `computed(name, computeFn, options?)` - Create a derived value | ||
- `react(name, effectFn, options?)` - Create a side effect | ||
- `transact(fn)` - Batch state updates | ||
### Class-based APIs | ||
- `@computed` - Decorator for computed class properties | ||
### Advanced | ||
- `reactor(name, effectFn)` - Create a controllable reaction | ||
- `unsafe__withoutCapture(fn)` - Read state without creating dependencies | ||
- `whyAmIRunning()` - Debug what triggered an update | ||
- `getComputedInstance(obj, prop)` - Get underlying computed instance | ||
- `getGlobalEpoch()` - Get current time for history tracking | ||
## Related Packages | ||
- **[@tldraw/state-react](../state-react)** - React bindings for @tldraw/state | ||
- **[@tldraw/store](../store)** - Record storage built on @tldraw/state | ||
- **[@tldraw/editor](../editor)** - The tldraw canvas editor | ||
- **[@tldraw/tldraw](../tldraw)** - Complete tldraw UI components | ||
## Examples & Patterns | ||
Looking for more examples? Check out: | ||
- [tldraw SDK examples](https://github.com/tldraw/tldraw/tree/main/apps/examples) - Real-world usage in tldraw applications | ||
## Contributing | ||
Please see our [contributing guide](https://github.com/tldraw/tldraw/blob/main/CONTRIBUTING.md). Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new). | ||
@@ -13,3 +276,3 @@ | ||
This project is provided under the MIT license found [here](https://github.com/tldraw/tldraw/blob/main/packages/state/LICENSE.md). | ||
This project is licensed under the MIT License found [here](https://github.com/tldraw/tldraw/blob/main/packages/state/LICENSE.md). The tldraw SDK is provided under the [tldraw license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md). | ||
@@ -22,3 +285,3 @@ ## Trademarks | ||
Find us on Twitter/X at [@tldraw](https://twitter.com/tldraw). | ||
Find us on Twitter/X at [@tldraw](https://twitter.com/tldraw). You can contact us by email at [hello@tldraw.com](mailto:hello@tldraw.com). | ||
@@ -25,0 +288,0 @@ ## Community |
@@ -33,3 +33,3 @@ import { ArraySet } from './ArraySet' | ||
* @param b - The new value | ||
* @returns | ||
* @returns True if the values are equal, false otherwise. | ||
*/ | ||
@@ -36,0 +36,0 @@ isEqual?(a: any, b: any): boolean |
@@ -108,11 +108,11 @@ /* eslint-disable prefer-rest-params */ | ||
/** | ||
* A method used to compute a diff between the atom's old and new values. If provided, it will not be used unless you also specify {@link AtomOptions.historyLength}. | ||
* A method used to compute a diff between the computed's old and new values. If provided, it will not be used unless you also specify {@link ComputedOptions.historyLength}. | ||
*/ | ||
computeDiff?: ComputeDiff<Value, Diff> | ||
/** | ||
* If provided, this will be used to compare the old and new values of the atom to determine if the value has changed. | ||
* If provided, this will be used to compare the old and new values of the computed to determine if the value has changed. | ||
* By default, values are compared using first using strict equality (`===`), then `Object.is`, and finally any `.equals` method present in the object's prototype chain. | ||
* @param a - The old value | ||
* @param b - The new value | ||
* @returns | ||
* @returns True if the values are equal, false otherwise. | ||
*/ | ||
@@ -119,0 +119,0 @@ isEqual?(a: any, b: any): boolean |
@@ -37,3 +37,3 @@ import { ArraySet } from './ArraySet' | ||
* @param execute - A function that will execute the effect. | ||
* @returns | ||
* @returns void | ||
*/ | ||
@@ -45,2 +45,3 @@ // eslint-disable-next-line @typescript-eslint/method-signature-style | ||
class __EffectScheduler__<Result> implements EffectScheduler<Result> { | ||
/** @internal */ | ||
private _isActivelyListening = false | ||
@@ -58,4 +59,8 @@ /** | ||
/** @internal */ | ||
private lastReactedEpoch = GLOBAL_START_EPOCH | ||
/** @internal */ | ||
private _scheduleCount = 0 | ||
/** @internal */ | ||
__debug_ancestor_epochs__: Map<Signal<any, any>, number> | null = null | ||
@@ -78,2 +83,3 @@ | ||
readonly parents: Signal<any, any>[] = [] | ||
/** @internal */ | ||
private readonly _scheduleEffect?: (execute: () => void) => void | ||
@@ -140,2 +146,3 @@ constructor( | ||
* It will no longer be eligible to receive 'maybeScheduleEffect' calls until `EffectScheduler.attach` is called again. | ||
* @public | ||
*/ | ||
@@ -152,2 +159,3 @@ detach() { | ||
* @returns The result of the effect. | ||
* @public | ||
*/ | ||
@@ -199,3 +207,2 @@ execute(): Result { | ||
export interface EffectScheduler<Result> { | ||
/** @internal */ | ||
/** | ||
@@ -251,2 +258,3 @@ * Whether this scheduler is attached and actively listening to its parents. | ||
* It will no longer be eligible to receive 'maybeScheduleEffect' calls until `EffectScheduler.attach` is called again. | ||
* @public | ||
*/ | ||
@@ -258,2 +266,3 @@ detach(): void | ||
* @returns The result of the effect. | ||
* @public | ||
*/ | ||
@@ -260,0 +269,0 @@ execute(): Result |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
429763
1.92%7378
0.31%289
1011.54%+ Added
- Removed