Overview
The useTraceListeners hook is a development tool that logs all observables being tracked by a component. It helps you understand exactly what state changes will cause re-renders.
This hook only works in development and test environments (NODE_ENV === 'development' or 'test').
Signature
function useTraceListeners(name?: string): void
Optional name to identify the component in log output
Usage
Call useTraceListeners inside an observer component or useSelector hook to see what observables are being tracked:
import { observer, useTraceListeners } from '@legendapp/state/react';
import { observable } from '@legendapp/state';
const state$ = observable({
user: { name: 'Alice', email: '[email protected]' },
count: 0,
settings: { theme: 'dark' }
});
const UserProfile = observer(function UserProfile() {
useTraceListeners('UserProfile');
const name = state$.user.name.get();
const theme = state$.settings.theme.get();
return (
<div className={theme}>
<h1>{name}</h1>
</div>
);
});
// Console output:
// [legend-state] UserProfile tracking 2 observables:
// 1: user.name
// 2: settings.theme
The hook logs a message showing:
- Component name (if provided)
- Number of observables being tracked
- Path to each observable
- Tracking mode (shallow or optimized)
[legend-state] ComponentName tracking 3 observables:
1: user.profile.name
2: items[0].title (shallow)
3: settings.* (optimized)
Tracking Modes
- (no suffix) - Deep tracking (default)
- (shallow) - Shallow tracking (only direct value changes)
- (optimized) - Optimized tracking for primitives
Advanced Usage
Debugging Re-renders
import { observer, useTraceListeners } from '@legendapp/state/react';
import { observable } from '@legendapp/state';
const store$ = observable({
products: [],
filters: { category: 'all', priceRange: [0, 100] },
ui: { loading: false }
});
const ProductList = observer(() => {
// Add trace to debug unexpected re-renders
useTraceListeners('ProductList');
const products = store$.products.get();
const loading = store$.ui.loading.get();
const category = store$.filters.category.get();
// Console will show exactly what this component tracks
// Helps identify if you're tracking too much state
return loading ? <Spinner /> : <div>{/* products */}</div>;
});
Multiple Components
import { observer, useTraceListeners } from '@legendapp/state/react';
import { observable } from '@legendapp/state';
const app$ = observable({
sidebar: { open: false },
header: { title: 'Dashboard' },
content: { data: [] }
});
const Sidebar = observer(() => {
useTraceListeners('Sidebar');
const open = app$.sidebar.open.get();
return <aside>{open && 'Menu'}</aside>;
});
const Header = observer(() => {
useTraceListeners('Header');
const title = app$.header.title.get();
return <header>{title}</header>;
});
const Content = observer(() => {
useTraceListeners('Content');
const data = app$.content.data.get();
return <main>{data.length} items</main>;
});
// Each component logs its own tracking info independently
With useSelector
import { useSelector, useTraceListeners } from '@legendapp/state/react';
import { observable } from '@legendapp/state';
const state$ = observable({ count: 0, name: 'Test' });
function Counter() {
const count = useSelector(() => {
useTraceListeners('Counter selector');
return state$.count.get();
});
return <div>Count: {count}</div>;
}
Comparison with useTraceUpdates
Both hooks help with debugging, but serve different purposes:
useTraceListeners
- Shows what observables are being tracked
- Logs once when component renders
- Helps identify over-tracking or missing tracking
- Use to understand component dependencies
useTraceUpdates
- Shows why a component re-rendered
- Logs on every state change
- Shows old and new values
- Use to debug specific re-render issues
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, useTraceListeners } from '@legendapp/state/react';
import { observable } from '@legendapp/state';
const store$ = observable({
users: [], // Large array
currentUser: { id: 1, name: 'Alice' },
ui: { /* ... */ }
});
const UserBadge = observer(() => {
useTraceListeners('UserBadge');
// If this logs "users" in the tracking list,
// the component will re-render on ANY user change!
// Fix by tracking only currentUser
const name = store$.currentUser.name.get(); // ✅ Good
// const users = store$.users.get(); // ❌ Would track entire array
return <div>{name}</div>;
});
Verifying Selective Tracking
import { observer, useTraceListeners } from '@legendapp/state/react';
import { observable } from '@legendapp/state';
const todos$ = observable([
{ id: 1, text: 'Task 1', done: false },
{ id: 2, text: 'Task 2', done: true }
]);
const TodoItem = observer(({ id }: { id: number }) => {
useTraceListeners(`TodoItem-${id}`);
const todo = todos$.find(t => t.id === id);
const text = todo.text.get();
const done = todo.done.get();
// Should log: todos[0].text, todos[0].done
// If it logs just "todos", you're tracking too much!
return (
<li>
<input type="checkbox" checked={done} />
{text}
</li>
);
});
TypeScript
The hook has full TypeScript support:
import { useTraceListeners } from '@legendapp/state/react';
function MyComponent() {
useTraceListeners(); // No name
useTraceListeners('MyComponent'); // With name
// useTraceListeners(123); // ❌ Error: must be string
// ...
}
Best Practices
Use during development: Enable tracing when debugging, but remove or comment out for production builds.
Name your components: Always provide a name parameter to easily identify components in logs.
Check tracking scope: If a component tracks too many observables, it will re-render unnecessarily. Use more specific selectors.
Combine with useTraceUpdates: Use both hooks together for complete debugging - one shows what’s tracked, the other shows what changed.
Production Builds
The hook is automatically disabled in production:
// This does nothing in production
useTraceListeners('Component');
No need to remove the calls or use conditional logic - they’re no-ops outside of development/test environments.