snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
819
packages/state-react/DOCS.md
Normal file
819
packages/state-react/DOCS.md
Normal file
@@ -0,0 +1,819 @@
|
||||
# @tldraw/state-react Documentation
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
### What is @tldraw/state-react?
|
||||
|
||||
@tldraw/state-react is a React integration library that bridges the reactive **signals** system from @tldraw/state with React components. It provides hooks and utilities for using reactive state in React applications with automatic dependency tracking and fine-grained updates.
|
||||
|
||||
This library extends [@tldraw/state](../state/DOCS.md) with React-specific bindings, offering familiar hook patterns while maintaining the performance characteristics of the underlying signals system.
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install @tldraw/state-react @tldraw/state react
|
||||
```
|
||||
|
||||
### TypeScript
|
||||
|
||||
@tldraw/state-react is written in TypeScript and provides excellent type safety out of the box. No additional types package needed.
|
||||
|
||||
### Quick Example
|
||||
|
||||
Here's a simple counter to show how @tldraw/state-react works:
|
||||
|
||||
```ts
|
||||
import { useAtom, track } from '@tldraw/state-react'
|
||||
|
||||
const Counter = track(function Counter() {
|
||||
const count = useAtom('count', 0)
|
||||
|
||||
return (
|
||||
<button onClick={() => count.set(count.get() + 1)}>
|
||||
Count: {count.get()}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
The `track` function automatically detects when signals are accessed and re-renders the component only when those specific signals change.
|
||||
|
||||
## 2. Core Hooks
|
||||
|
||||
### useValue: Reading Signal Values
|
||||
|
||||
**useValue** is the fundamental hook for extracting values from signals and subscribing to changes. It comes in two forms: direct signal subscription and computed values.
|
||||
|
||||
#### Reading Signal Values
|
||||
|
||||
You can use `useValue` to extract the current value from any signal:
|
||||
|
||||
```ts
|
||||
import { atom } from '@tldraw/state'
|
||||
import { useValue } from '@tldraw/state-react'
|
||||
|
||||
const name = atom('name', 'World')
|
||||
|
||||
function Greeter() {
|
||||
const currentName = useValue(name)
|
||||
return <h1>Hello, {currentName}!</h1>
|
||||
}
|
||||
```
|
||||
|
||||
When `name` changes, the `Greeter` component automatically re-renders with the new value.
|
||||
|
||||
#### Creating Computed Values
|
||||
|
||||
You can also pass a function to `useValue` to create computed values with automatic dependency tracking:
|
||||
|
||||
```ts
|
||||
import { atom } from '@tldraw/state'
|
||||
import { useValue } from '@tldraw/state-react'
|
||||
|
||||
const firstName = atom('firstName', 'John')
|
||||
const lastName = atom('lastName', 'Doe')
|
||||
|
||||
function UserProfile() {
|
||||
const fullName = useValue('fullName', () => {
|
||||
return `${firstName.get()} ${lastName.get()}`
|
||||
}, [firstName, lastName])
|
||||
|
||||
return <div>User: {fullName}</div>
|
||||
}
|
||||
```
|
||||
|
||||
> Tip: The dependency array works like other React hooks - include all signals that your computed function depends on.
|
||||
|
||||
### useAtom: Component-Local Reactive State
|
||||
|
||||
**useAtom** creates reactive atoms that are scoped to your component instance. This is perfect for component-local state that you want to be reactive.
|
||||
|
||||
#### Creating Local Atoms
|
||||
|
||||
```ts
|
||||
import { useAtom } from '@tldraw/state-react'
|
||||
|
||||
function TodoItem() {
|
||||
const completed = useAtom('completed', false)
|
||||
const text = useAtom('text', 'New todo')
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={completed.get()}
|
||||
onChange={(e) => completed.set(e.target.checked)}
|
||||
/>
|
||||
<input
|
||||
value={text.get()}
|
||||
onChange={(e) => text.set(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### Lazy Initialization
|
||||
|
||||
You can pass a function as the initial value for expensive computations:
|
||||
|
||||
```ts
|
||||
function DataProcessor() {
|
||||
const expensiveData = useAtom('data', () => {
|
||||
// This function only runs once when the component mounts
|
||||
return processLargeDataset()
|
||||
})
|
||||
|
||||
return <div>Processing {expensiveData.get().length} items</div>
|
||||
}
|
||||
```
|
||||
|
||||
#### Atom Options
|
||||
|
||||
You can customize atom behavior with options:
|
||||
|
||||
```ts
|
||||
const user = useAtom(
|
||||
'user',
|
||||
{ id: 1, name: 'Alice' },
|
||||
{
|
||||
isEqual: (a, b) => a.id === b.id, // Only update if ID changes
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### useComputed: Component-Local Computed Values
|
||||
|
||||
**useComputed** creates computed signals that automatically track their dependencies and recalculate when needed.
|
||||
|
||||
#### Basic Computed Values
|
||||
|
||||
```ts
|
||||
import { useAtom, useComputed } from '@tldraw/state-react'
|
||||
|
||||
function ShoppingCart() {
|
||||
const items = useAtom('items', [])
|
||||
const total = useComputed('total', () => {
|
||||
return items.get().reduce((sum, item) => sum + item.price, 0)
|
||||
}, [items])
|
||||
|
||||
return <div>Total: ${total.get().toFixed(2)}</div>
|
||||
}
|
||||
```
|
||||
|
||||
#### Advanced Computed Options
|
||||
|
||||
You can provide options for custom equality checking and diff computation:
|
||||
|
||||
```ts
|
||||
const optimizedData = useComputed(
|
||||
'processed',
|
||||
() => {
|
||||
return heavyProcessing(rawData.get())
|
||||
},
|
||||
{
|
||||
isEqual: (a, b) => a.checksum === b.checksum,
|
||||
},
|
||||
[rawData]
|
||||
)
|
||||
```
|
||||
|
||||
> Tip: Use computed values to avoid expensive recalculations. They only recalculate when their dependencies actually change.
|
||||
|
||||
## 3. Component Tracking
|
||||
|
||||
### track: Automatic Signal Tracking
|
||||
|
||||
The **track** higher-order component is the most convenient way to make React components reactive. It automatically tracks which signals your component accesses and re-renders when any of them change.
|
||||
|
||||
#### Basic Component Tracking
|
||||
|
||||
```ts
|
||||
import { atom } from '@tldraw/state'
|
||||
import { track } from '@tldraw/state-react'
|
||||
|
||||
const theme = atom('theme', 'light')
|
||||
const userName = atom('userName', 'Guest')
|
||||
|
||||
const Header = track(function Header() {
|
||||
return (
|
||||
<header className={theme.get()}>
|
||||
Welcome, {userName.get()}!
|
||||
</header>
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
Now whenever `theme` or `userName` changes, the `Header` component automatically re-renders.
|
||||
|
||||
#### Tracking with Props
|
||||
|
||||
Track works seamlessly with component props and React patterns:
|
||||
|
||||
```ts
|
||||
interface UserCardProps {
|
||||
userId: string
|
||||
}
|
||||
|
||||
const UserCard = track(function UserCard({ userId }: UserCardProps) {
|
||||
const user = useValue('user', () => getUserById(userId), [userId])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>{user.name}</h3>
|
||||
<p>Email: {user.email}</p>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
#### Tracking and React.memo
|
||||
|
||||
The `track` function automatically wraps your component in `React.memo`, so it only re-renders when props change or tracked signals change:
|
||||
|
||||
```ts
|
||||
// This component only re-renders when:
|
||||
// 1. The userId prop changes, OR
|
||||
// 2. The signals accessed inside the component change
|
||||
const OptimizedUserCard = track(function OptimizedUserCard({ userId }: UserCardProps) {
|
||||
const user = getUserAtom(userId) // Signal access is tracked
|
||||
return <div>{user.get().name}</div>
|
||||
})
|
||||
```
|
||||
|
||||
### useStateTracking: Manual Tracking
|
||||
|
||||
For more control, you can use **useStateTracking** to manually track signal dependencies in specific parts of your render function. It also accepts an optional dependency array, similar to `useMemo`, to control when the reactive tracking logic is re-created.
|
||||
|
||||
```ts
|
||||
import { useStateTracking } from '@tldraw/state-react'
|
||||
|
||||
function CustomComponent() {
|
||||
const [regularState, setRegularState] = useState(0)
|
||||
|
||||
const reactiveContent = useStateTracking('reactive-section', () => {
|
||||
// Only this part is reactive to signals
|
||||
return <div>Current theme: {theme.get()}</div>
|
||||
}, []) // deps array is optional
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setRegularState(s => s + 1)}>
|
||||
Regular state: {regularState}
|
||||
</button>
|
||||
{reactiveContent}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
> Tip: Use `useStateTracking` when you need fine-grained control over which parts of your component are reactive.
|
||||
|
||||
## 4. Side Effects and Reactions
|
||||
|
||||
Reading and displaying reactive state is only part of the story. The other part is performing **side effects** that respond to state changes - like updating the DOM, making network requests, or triggering animations.
|
||||
|
||||
### useReactor: Frame-Throttled Effects
|
||||
|
||||
**useReactor** runs side effects in response to signal changes, with updates throttled to animation frames for optimal performance:
|
||||
|
||||
```ts
|
||||
import { useReactor } from '@tldraw/state-react'
|
||||
|
||||
function CanvasRenderer() {
|
||||
const shapes = useAtom('shapes', [])
|
||||
|
||||
useReactor('canvas-update', () => {
|
||||
// This runs at most once per animation frame
|
||||
redrawCanvas(shapes.get())
|
||||
}, [shapes])
|
||||
|
||||
return <canvas ref={canvasRef} />
|
||||
}
|
||||
```
|
||||
|
||||
The effect runs immediately when the component mounts, then again whenever `shapes` changes, but updates are batched to animation frames for smooth performance.
|
||||
|
||||
#### Visual Updates and Animations
|
||||
|
||||
Use `useReactor` for any visual updates or DOM manipulations:
|
||||
|
||||
```ts
|
||||
function AnimatedCounter() {
|
||||
const count = useAtom('count', 0)
|
||||
const elementRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useReactor('animate-color', () => {
|
||||
const element = elementRef.current
|
||||
if (element) {
|
||||
// Animate background color based on count
|
||||
element.style.backgroundColor = count.get() > 10 ? 'green' : 'blue'
|
||||
}
|
||||
}, [count])
|
||||
|
||||
return (
|
||||
<div ref={elementRef}>
|
||||
<button onClick={() => count.set(count.get() + 1)}>
|
||||
Count: {count.get()}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useQuickReactor: Immediate Effects
|
||||
|
||||
**useQuickReactor** runs side effects immediately without throttling, perfect for critical updates that can't wait:
|
||||
|
||||
```ts
|
||||
import { useQuickReactor } from '@tldraw/state-react'
|
||||
|
||||
function DataSynchronizer() {
|
||||
const criticalData = useAtom('criticalData', null)
|
||||
|
||||
useQuickReactor('sync-data', () => {
|
||||
const data = criticalData.get()
|
||||
if (data) {
|
||||
// Send immediately - don't wait for next frame
|
||||
sendToServer(data)
|
||||
}
|
||||
}, [criticalData])
|
||||
|
||||
return <div>Sync status updated</div>
|
||||
}
|
||||
```
|
||||
|
||||
#### When to Use Quick vs Throttled Effects
|
||||
|
||||
**Use `useReactor` (throttled) for:**
|
||||
|
||||
- Visual updates and animations
|
||||
- DOM manipulations
|
||||
- Canvas rendering
|
||||
- UI state changes
|
||||
|
||||
**Use `useQuickReactor` (immediate) for:**
|
||||
|
||||
- Data synchronization
|
||||
- Network requests
|
||||
- Critical state updates
|
||||
- Event logging
|
||||
|
||||
```ts
|
||||
function ComprehensiveExample() {
|
||||
const userInput = useAtom('userInput', '')
|
||||
const selectedItems = useAtom('selectedItems', [])
|
||||
|
||||
// Throttled: Visual feedback
|
||||
useReactor('visual-feedback', () => {
|
||||
updateHighlightedElements(selectedItems.get())
|
||||
}, [selectedItems])
|
||||
|
||||
// Immediate: Data persistence
|
||||
useQuickReactor('save-draft', () => {
|
||||
saveDraft(userInput.get())
|
||||
}, [userInput])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input onChange={(e) => userInput.set(e.target.value)} />
|
||||
{/* ... */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Advanced Patterns
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
While @tldraw/state-react is optimized by default, there are patterns for demanding applications.
|
||||
|
||||
#### Minimizing Re-renders with Selective Tracking
|
||||
|
||||
Use `track` strategically to only make components reactive when needed:
|
||||
|
||||
```ts
|
||||
// Only the inner component is reactive
|
||||
function UserDashboard({ userId }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<StaticHeader />
|
||||
<UserContent userId={userId} /> {/* This is tracked */}
|
||||
<StaticFooter />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const UserContent = track(function UserContent({ userId }: Props) {
|
||||
const user = getUserSignal(userId)
|
||||
return <div>{user.get().name}</div>
|
||||
})
|
||||
```
|
||||
|
||||
#### Batching Updates with Multiple Signals
|
||||
|
||||
When you need to update multiple related signals, use transactions from @tldraw/state:
|
||||
|
||||
```ts
|
||||
import { transact } from '@tldraw/state'
|
||||
|
||||
function BulkUpdater() {
|
||||
const firstName = useAtom('firstName', '')
|
||||
const lastName = useAtom('lastName', '')
|
||||
const email = useAtom('email', '')
|
||||
|
||||
const updateUser = (userData: UserData) => {
|
||||
transact(() => {
|
||||
// All three updates happen atomically
|
||||
firstName.set(userData.firstName)
|
||||
lastName.set(userData.lastName)
|
||||
email.set(userData.email)
|
||||
})
|
||||
// Components re-render only once after all changes
|
||||
}
|
||||
|
||||
return <button onClick={() => updateUser(newData)}>Update User</button>
|
||||
}
|
||||
```
|
||||
|
||||
### Integration with External State
|
||||
|
||||
#### Syncing with External Systems
|
||||
|
||||
You can use reactive effects to sync with external systems:
|
||||
|
||||
```ts
|
||||
function LocalStorageSync() {
|
||||
const preferences = useAtom('preferences', {})
|
||||
|
||||
// Save to localStorage when preferences change
|
||||
useQuickReactor('save-preferences', () => {
|
||||
localStorage.setItem('prefs', JSON.stringify(preferences.get()))
|
||||
}, [preferences])
|
||||
|
||||
// Load from localStorage on mount
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem('prefs')
|
||||
if (saved) {
|
||||
preferences.set(JSON.parse(saved))
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <div>Preferences synced!</div>
|
||||
}
|
||||
```
|
||||
|
||||
#### WebSocket Integration
|
||||
|
||||
```ts
|
||||
function RealtimeData() {
|
||||
const liveData = useAtom('liveData', {})
|
||||
|
||||
useEffect(() => {
|
||||
const ws = new WebSocket('ws://localhost:8080')
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data)
|
||||
liveData.set(data) // Updates trigger reactive re-renders
|
||||
}
|
||||
|
||||
return () => ws.close()
|
||||
}, [])
|
||||
|
||||
return <div>Live data: {JSON.stringify(liveData.get())}</div>
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Hook Patterns
|
||||
|
||||
You can create custom hooks that combine multiple state-react hooks:
|
||||
|
||||
```ts
|
||||
function useCounter(initialValue = 0) {
|
||||
const count = useAtom('count', initialValue)
|
||||
|
||||
const increment = useCallback(() => count.update(n => n + 1), [count])
|
||||
const decrement = useCallback(() => count.update(n => n - 1), [count])
|
||||
const reset = useCallback(() => count.set(initialValue), [count, initialValue])
|
||||
|
||||
return {
|
||||
count: count.get(),
|
||||
increment,
|
||||
decrement,
|
||||
reset
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const CounterComponent = track(function CounterComponent() {
|
||||
const { count, increment, decrement, reset } = useCounter(10)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>{count}</span>
|
||||
<button onClick={increment}>+</button>
|
||||
<button onClick={decrement}>-</button>
|
||||
<button onClick={reset}>Reset</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
## 6. Debugging and Development
|
||||
|
||||
Because @tldraw/state-react builds on the reactive signals from @tldraw/state, you get access to powerful debugging tools for understanding component behavior.
|
||||
|
||||
### Using whyAmIRunning
|
||||
|
||||
You can use `whyAmIRunning()` from @tldraw/state to debug why components are re-rendering:
|
||||
|
||||
```ts
|
||||
import { whyAmIRunning } from '@tldraw/state'
|
||||
|
||||
const DebuggableComponent = track(function DebuggableComponent() {
|
||||
const userStatus = useValue(currentUser, user => user.status, [currentUser])
|
||||
const themeColor = useValue(appTheme, theme => theme.primaryColor, [appTheme])
|
||||
|
||||
// Debug why this component is re-rendering
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
whyAmIRunning()
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ color: themeColor }}>
|
||||
Status: {userStatus}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
When the component re-renders, you'll see output like:
|
||||
|
||||
```
|
||||
TrackedComponent is executing because:
|
||||
↳ Computed(user.status) changed
|
||||
↳ Atom(currentUser) changed
|
||||
```
|
||||
|
||||
### Debugging Reactive Effects
|
||||
|
||||
You can debug reactive effects by adding logging:
|
||||
|
||||
```ts
|
||||
function DebuggableEffects() {
|
||||
const data = useAtom('data', [])
|
||||
|
||||
useReactor('debug-data-changes', () => {
|
||||
console.log('Data changed:', data.get())
|
||||
console.log('Change triggered at:', new Date().toISOString())
|
||||
|
||||
// Use whyAmIRunning to see what caused this effect
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
whyAmIRunning()
|
||||
}
|
||||
}, [data])
|
||||
|
||||
return <div>Check console for debug info</div>
|
||||
}
|
||||
```
|
||||
|
||||
### Component Performance Monitoring
|
||||
|
||||
Track renders and signal accesses in development:
|
||||
|
||||
```ts
|
||||
const MonitoredComponent = track(function MonitoredComponent({ userId }: Props) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('Component rendering for user:', userId)
|
||||
}
|
||||
|
||||
const user = useValue('user', () => {
|
||||
console.log('Fetching user data...')
|
||||
return getUserById(userId)
|
||||
}, [userId])
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('User data:', user)
|
||||
}
|
||||
|
||||
return <div>User: {user.name}</div>
|
||||
})
|
||||
```
|
||||
|
||||
### React DevTools Integration
|
||||
|
||||
@tldraw/state-react works seamlessly with React DevTools:
|
||||
|
||||
- Components wrapped with `track` appear as "Memo(ComponentName)"
|
||||
- Signal updates trigger normal React re-render detection
|
||||
- Props changes are tracked separately from signal changes
|
||||
- Use React DevTools Profiler to identify performance bottlenecks
|
||||
|
||||
> Tip: Enable "Highlight when components render" in React DevTools to see which components re-render when signals change.
|
||||
|
||||
## 7. Best Practices and Patterns
|
||||
|
||||
### Component Organization
|
||||
|
||||
Structure your reactive components for maintainability:
|
||||
|
||||
```ts
|
||||
// ❌ Avoid: Large components with mixed concerns
|
||||
const MonolithicComponent = track(function MonolithicComponent() {
|
||||
const user = useAtom('user', {})
|
||||
const posts = useAtom('posts', [])
|
||||
const comments = useAtom('comments', [])
|
||||
const theme = useAtom('theme', 'light')
|
||||
|
||||
// 100+ lines of mixed logic...
|
||||
})
|
||||
|
||||
// ✅ Better: Split into focused components
|
||||
const UserProfile = track(function UserProfile() {
|
||||
const user = useAtom('user', {})
|
||||
return <UserInfo user={user.get()} />
|
||||
})
|
||||
|
||||
const PostsList = track(function PostsList() {
|
||||
const posts = useAtom('posts', [])
|
||||
return <PostList posts={posts.get()} />
|
||||
})
|
||||
```
|
||||
|
||||
### Signal Naming Conventions
|
||||
|
||||
Use consistent naming for clarity:
|
||||
|
||||
```ts
|
||||
// ✅ Good: Descriptive names with context
|
||||
const currentUser = useAtom('currentUser', null)
|
||||
const selectedShapes = useAtom('selectedShapes', [])
|
||||
const editorMode = useAtom('editorMode', 'select')
|
||||
|
||||
// ❌ Avoid: Generic names without context
|
||||
const data = useAtom('data', null)
|
||||
const state = useAtom('state', {})
|
||||
const items = useAtom('items', [])
|
||||
```
|
||||
|
||||
### Effect Organization
|
||||
|
||||
Keep effects focused and well-named:
|
||||
|
||||
```ts
|
||||
function WellOrganizedComponent() {
|
||||
const shapes = useAtom('shapes', [])
|
||||
const camera = useAtom('camera', { x: 0, y: 0, z: 1 })
|
||||
|
||||
// Visual updates - throttled
|
||||
useReactor('update-viewport', () => {
|
||||
updateViewportTransform(camera.get())
|
||||
}, [camera])
|
||||
|
||||
useReactor('render-shapes', () => {
|
||||
renderShapes(shapes.get())
|
||||
}, [shapes])
|
||||
|
||||
// Data persistence - immediate
|
||||
useQuickReactor('save-document', () => {
|
||||
saveDocument({ shapes: shapes.get(), camera: camera.get() })
|
||||
}, [shapes, camera])
|
||||
|
||||
return <canvas />
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
Handle errors gracefully in reactive code:
|
||||
|
||||
```ts
|
||||
const SafeDataComponent = track(function SafeDataComponent() {
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
const userData = useValue('userData', () => {
|
||||
try {
|
||||
return processUserData(rawUserData.get())
|
||||
} catch (err) {
|
||||
setError(err)
|
||||
return null
|
||||
}
|
||||
}, [rawUserData])
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage error={error} />
|
||||
}
|
||||
|
||||
return <UserDisplay data={userData} />
|
||||
})
|
||||
```
|
||||
|
||||
### Testing Reactive Components
|
||||
|
||||
Test reactive components by testing signal changes:
|
||||
|
||||
```ts
|
||||
import { render, act } from '@testing-library/react'
|
||||
import { atom } from '@tldraw/state'
|
||||
|
||||
test('component updates when signal changes', () => {
|
||||
const nameSignal = atom('name', 'Initial')
|
||||
|
||||
const TestComponent = track(function TestComponent() {
|
||||
return <div data-testid="name">{nameSignal.get()}</div>
|
||||
})
|
||||
|
||||
const { getByTestId } = render(<TestComponent />)
|
||||
|
||||
expect(getByTestId('name')).toHaveTextContent('Initial')
|
||||
|
||||
act(() => {
|
||||
nameSignal.set('Updated')
|
||||
})
|
||||
|
||||
expect(getByTestId('name')).toHaveTextContent('Updated')
|
||||
})
|
||||
```
|
||||
|
||||
## 8. Integration with @tldraw/state
|
||||
|
||||
@tldraw/state-react is designed to work seamlessly with the core @tldraw/state library. You can use all the features of @tldraw/state within React components.
|
||||
|
||||
### Using Atoms and Computed Values
|
||||
|
||||
```ts
|
||||
import { atom, computed } from '@tldraw/state'
|
||||
import { track, useValue } from '@tldraw/state-react'
|
||||
|
||||
// Create signals outside of components for global state
|
||||
const firstName = atom('firstName', 'John')
|
||||
const lastName = atom('lastName', 'Doe')
|
||||
const fullName = computed('fullName', () => `${firstName.get()} ${lastName.get()}`)
|
||||
|
||||
const UserGreeting = track(function UserGreeting() {
|
||||
// Access global signals in components
|
||||
const name = fullName.get()
|
||||
return <h1>Hello, {name}!</h1>
|
||||
})
|
||||
```
|
||||
|
||||
### Transactions and Batching
|
||||
|
||||
Use transactions to batch multiple updates:
|
||||
|
||||
```ts
|
||||
import { transact } from '@tldraw/state'
|
||||
|
||||
function BatchedUpdates() {
|
||||
const user = useAtom('user', { name: '', email: '' })
|
||||
|
||||
const updateUser = (newData: UserData) => {
|
||||
transact(() => {
|
||||
// Multiple updates happen atomically
|
||||
user.update(current => ({ ...current, name: newData.name }))
|
||||
user.update(current => ({ ...current, email: newData.email }))
|
||||
})
|
||||
// Component only re-renders once after both changes
|
||||
}
|
||||
|
||||
return <button onClick={() => updateUser(formData)}>Update</button>
|
||||
}
|
||||
```
|
||||
|
||||
### History and Time Travel
|
||||
|
||||
Access signal history for undo/redo functionality:
|
||||
|
||||
```ts
|
||||
import { atom } from '@tldraw/state'
|
||||
import { useAtom } from '@tldraw/state-react'
|
||||
|
||||
function UndoableEditor() {
|
||||
// Create an atom with history enabled
|
||||
const content = useAtom('content', '', { historyLength: 10 })
|
||||
|
||||
const undo = () => {
|
||||
// The history is stored on the atom and can be accessed
|
||||
// to implement undo/redo functionality. The exact implementation
|
||||
// depends on your diff format and how you use the history API
|
||||
// from @tldraw/state.
|
||||
// e.g. `const diffs = content.getDiffSince(someEpoch)`
|
||||
console.log('Undo clicked. See @tldraw/state docs for implementation.')
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<textarea
|
||||
value={content.get()}
|
||||
onChange={(e) => content.set(e.target.value)}
|
||||
/>
|
||||
<button onClick={undo}>Undo</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
> Tip: See the [@tldraw/state documentation](../state/DOCS.md) for complete details on transactions, history, and advanced signal features.
|
||||
|
||||
This powerful combination of @tldraw/state and @tldraw/state-react gives you a complete reactive state solution that scales from simple components to complex applications like the tldraw editor itself.
|
||||
Reference in New Issue
Block a user