Socket
Book a DemoInstallSign in
Socket

@tldraw/state

Package Overview
Dependencies
Maintainers
4
Versions
2690
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@tldraw/state

tldraw infinite canvas SDK (state).

latest
Source
npmnpm
Version
4.0.2
Version published
Weekly downloads
62K
-28.93%
Maintainers
4
Weekly downloads
 
Created
Source

@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'

// 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:

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:

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:

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:

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:

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:

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:

react('debug-reaction', () => {
	whyAmIRunning() // Logs dependency tree to console
	// Your reaction code...
})

Integration Examples

With tldraw SDK

// 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:

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

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.

Community

Have questions, comments or feedback? Join our discord. For the latest news and release notes, visit tldraw.dev.

Keywords

tldraw

FAQs

Package last updated on 19 Sep 2025

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts