Overview
The useTraceUpdates hook is a development tool that logs whenever a tracked observable changes, showing you exactly why a component re-rendered. It displays the observable path, old value, and new value.
This hook only works in development and test environments (NODE_ENV === 'development' or 'test').
Signature
function useTraceUpdates(name?: string): void
Optional name to identify the component in log output
Usage
Call useTraceUpdates inside an observer component or useSelector hook to see why re-renders happen:
import { observer, useTraceUpdates } from '@legendapp/state/react';
import { observable } from '@legendapp/state';
const state$ = observable({
user: { name: 'Alice' },
count: 0
});
const UserProfile = observer(function UserProfile() {
useTraceUpdates('UserProfile');
const name = state$.user.name.get();
const count = state$.count.get();
return (
<div>
<h1>{name}</h1>
<p>Count: {count}</p>
</div>
);
});
// When state$.user.name changes:
// Console output:
// [legend-state] Rendering UserProfile because "user.name" changed:
// from: "Alice"
// to: "Bob"
// When state$.count changes:
// [legend-state] Rendering UserProfile because "count" changed:
// from: 0
// to: 1
When a tracked observable changes, the hook logs:
- Component name (if provided)
- Observable path that changed
- Previous value (serialized as JSON)
- New value (serialized as JSON)
[legend-state] Rendering ComponentName because "path.to.observable" changed:
from: {"old":"value"}
to: {"new":"value"}
Multiple Changes
If multiple observables change at once (e.g., in a batch), each change is logged separately:
import { observer, useTraceUpdates } from '@legendapp/state/react';
import { observable, beginBatch, endBatch } from '@legendapp/state';
const form$ = observable({
firstName: 'John',
lastName: 'Doe'
});
const Form = observer(() => {
useTraceUpdates('Form');
const firstName = form$.firstName.get();
const lastName = form$.lastName.get();
return <div>{firstName} {lastName}</div>;
});
// Batch multiple changes
beginBatch();
form$.firstName.set('Jane');
form$.lastName.set('Smith');
endBatch();
// Console output:
// [legend-state] Rendering Form because "firstName" changed:
// from: "John"
// to: "Jane"
// [legend-state] Rendering Form because "lastName" changed:
// from: "Doe"
// to: "Smith"
Advanced Usage
Debugging Unexpected Re-renders
import { observer, useTraceUpdates } from '@legendapp/state/react';
import { observable } from '@legendapp/state';
const store$ = observable({
products: [/* ... */],
filters: { search: '' },
ui: { loading: false }
});
const ProductList = observer(() => {
useTraceUpdates('ProductList');
const products = store$.products.get();
const loading = store$.ui.loading.get();
// If component re-renders unexpectedly,
// the console will show exactly which observable changed
return loading ? <Spinner /> : <List items={products} />;
});
// When store$.ui.loading changes:
// [legend-state] Rendering ProductList because "ui.loading" changed:
// from: false
// to: true
import { observer, useTraceUpdates } from '@legendapp/state/react';
import { observable } from '@legendapp/state';
const form$ = observable({
email: '',
password: '',
rememberMe: false
});
const LoginForm = observer(() => {
useTraceUpdates('LoginForm');
const email = form$.email.get();
const password = form$.password.get();
const rememberMe = form$.rememberMe.get();
// Each keystroke or checkbox change will be logged
return (
<form>
<input value={email} onChange={e => form$.email.set(e.target.value)} />
<input type="password" value={password} onChange={e => form$.password.set(e.target.value)} />
<input type="checkbox" checked={rememberMe} onChange={e => form$.rememberMe.set(e.target.checked)} />
</form>
);
});
With useSelector
import { useSelector, useTraceUpdates } from '@legendapp/state/react';
import { observable } from '@legendapp/state';
const state$ = observable({ items: [] });
function ItemCount() {
const count = useSelector(() => {
useTraceUpdates('ItemCount selector');
return state$.items.length;
});
return <div>{count} items</div>;
}
// When items array changes:
// [legend-state] Rendering ItemCount selector because "items" changed:
// from: []
// to: [{"id":1}]
Comparison with useTraceListeners
Both hooks help with debugging, but serve different purposes:
useTraceUpdates
- Shows why a component re-rendered
- Logs on every state change
- Shows old and new values
- Use to debug specific re-render issues
useTraceListeners
- Shows what observables are being tracked
- Logs once when component renders
- Lists all tracked observable paths
- Use to understand component dependencies
import { observer, useTraceListeners, useTraceUpdates } from '@legendapp/state/react';
const Component = observer(() => {
useTraceListeners('Component'); // What am I tracking?
useTraceUpdates('Component'); // Why did I re-render?
// ...
});
Real-World Examples
import { observer, useTraceUpdates } from '@legendapp/state/react';
import { observable } from '@legendapp/state';
const app$ = observable({
posts: [], // Large array
currentPost: { id: 1, title: 'Hello' }
});
const PostViewer = observer(() => {
useTraceUpdates('PostViewer');
const currentPost = app$.currentPost.get();
// If this re-renders when posts array changes,
// you'll see it in the console and can fix it
return <article>{currentPost.title}</article>;
});
// Bad: tracking entire posts array
const PostList = observer(() => {
useTraceUpdates('PostList');
const posts = app$.posts.get(); // This will log every time posts changes
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
});
import { observer, useTraceUpdates } from '@legendapp/state/react';
import { observable, computed } from '@legendapp/state';
const form$ = observable({
email: '',
password: ''
});
const isValid$ = computed(() => {
return form$.email.get().includes('@') && form$.password.get().length >= 8;
});
const SubmitButton = observer(() => {
useTraceUpdates('SubmitButton');
const isValid = isValid$.get();
// See exactly when and why validation state changes
return <button disabled={!isValid}>Submit</button>;
});
// Console shows:
// [legend-state] Rendering SubmitButton because "computed" changed:
// from: false
// to: true
Animation State Tracking
import { observer, useTraceUpdates } from '@legendapp/state/react';
import { observable } from '@legendapp/state';
const animation$ = observable({
isPlaying: false,
progress: 0,
duration: 1000
});
const AnimationController = observer(() => {
useTraceUpdates('AnimationController');
const isPlaying = animation$.isPlaying.get();
const progress = animation$.progress.get();
// Track every progress update during animation
return (
<div>
<div style={{ width: `${progress}%` }} />
<button onClick={() => animation$.isPlaying.set(!isPlaying)}>
{isPlaying ? 'Pause' : 'Play'}
</button>
</div>
);
});
TypeScript
The hook has full TypeScript support:
import { useTraceUpdates } from '@legendapp/state/react';
function MyComponent() {
useTraceUpdates(); // No name
useTraceUpdates('MyComponent'); // With name
// useTraceUpdates(123); // ❌ Error: must be string
// ...
}
Best Practices
Use during development: Enable tracing when debugging re-render issues, but remove for production.
Name your components: Always provide a name parameter to easily identify which component is re-rendering.
Combine with useTraceListeners: Use both hooks together for complete debugging:
useTraceListeners shows what’s being tracked
useTraceUpdates shows what changed
Be cautious with large values: The hook serializes values to JSON, which can be slow for very large objects or arrays.
Production Builds
The hook is automatically disabled in production:
// This does nothing in production
useTraceUpdates('Component');
No need to remove the calls or use conditional logic - they’re no-ops outside of development/test environments.