@tldraw/state
@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.
@tldraw/state
powers the reactive system at the heart of tldraw, handling everything from canvas updates to collaborative state synchronization. It's designed to work seamlessly with @tldraw/store and has optional React bindings.
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
npm install @tldraw/state
Quick Start
import { atom, computed, react } from '@tldraw/state'
const name = atom('name', 'World')
const count = atom('count', 0)
const greeting = computed('greeting', () => {
return `Hello, ${name.get()}! Count: ${count.get()}`
})
react('logger', () => {
console.log(greeting.get())
})
name.set('tldraw')
count.set(42)
Core Concepts
Atoms - State Containers
Atoms hold raw values and are the foundation of your reactive state:
import { atom } from '@tldraw/state'
const user = atom('user', { name: 'Alice', age: 30 })
const theme = atom('theme', 'light')
console.log(user.get().name)
user.update((current) => ({ ...current, age: 31 }))
theme.set('dark')
Computed Values - Automatic Derivation
Computed signals derive their values from other signals and update automatically:
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())
firstName.set('Jane')
console.log(fullName.get())
Reactions - Side Effects
Reactions run side effects when their dependencies change:
import { react } from '@tldraw/state'
const selectedId = atom('selectedId', null)
const stop = react('update-selection-ui', () => {
const id = selectedId.get()
document.getElementById('selected').textContent = id || 'None'
})
selectedId.set('shape-123')
stop()
Transactions - Batched Updates
Batch multiple updates to prevent intermediate reactions:
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()))
transact(() => {
x.set(10)
y.set(20)
})
Advanced Features
History & Time Travel
Track changes over time for undo/redo functionality:
const canvas = atom(
'canvas',
{ shapes: [] },
{
historyLength: 100,
computeDiff: (prev, next) => ({ prev, next }),
}
)
canvas.update((state) => ({ shapes: [...state.shapes, newShape] }))
const startTime = getGlobalEpoch()
const diffs = canvas.getDiffSince(startTime)
Performance Optimization
Use unsafe__withoutCapture
to read values without creating dependencies:
const expensiveComputed = computed('expensive', () => {
const important = importantValue.get()
const metadata = unsafe__withoutCapture(() => metadataAtom.get())
return computeExpensiveValue(important, metadata)
})
Debugging
Use whyAmIRunning()
to understand what triggered an update:
react('debug-reaction', () => {
whyAmIRunning()
})
Integration Examples
With tldraw SDK
const editor = useEditor()
const selectedShapes = computed('selectedShapes', () => {
return editor.getSelectedShapeIds().map((id) => editor.getShape(id))
})
react('update-property-panel', () => {
const shapes = selectedShapes.get()
updatePropertyPanel(shapes)
})
With React
Install the React bindings:
npm install @tldraw/state-react
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.
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
Examples & Patterns
Looking for more examples? Check out:
Contributing
Please see our contributing guide. Found a bug? Please submit an issue.
License
This project is licensed under the MIT License found here. The tldraw SDK is provided under the tldraw license.
Trademarks
Copyright (c) 2024-present tldraw Inc. The tldraw name and logo are trademarks of tldraw. Please see our trademark guidelines for info on acceptable usage.
Contact
Find us on Twitter/X at @tldraw. You can contact us by email at hello@tldraw.com.
Have questions, comments or feedback? Join our discord. For the latest news and release notes, visit tldraw.dev.