Skip to main content

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
name
string
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

Output Format

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

Debugging Performance Issues

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.

Build docs developers (and LLMs) love