Skip to main content

useTrackedState

Drop-in replacement for useState with automatic state tracking and debugging.

Overview

useTrackedState works exactly like React's useState but automatically tracks every state change, making it visible in the debugger timeline.

API

const [state, setState] = useTrackedState<T>(
initialState: T | (() => T),
label?: string
);

Parameters

  • initialState: Initial state value or lazy initializer function
  • label (optional): Human-readable label for the debugger (recommended)

Returns

Returns a stateful value and a function to update it, exactly like useState.

Basic Example

import { useTrackedState } from 'react-dev-debugger';

function Counter() {
const [count, setCount] = useTrackedState(0, 'counter');

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}

Live Playground

Examples

Simple Counter (Number)
Count: 0
const [count, setCount] = useTrackedState(0, 'counter');
setCount(count + 1);
String State
Name: (empty)
const [name, setName] = useTrackedState('', 'userName');
setName(e.target.value);
Object State
{ "name": "", "age": 0, "email": "" }
const [user, setUser] = useTrackedState({ 
  name: '', age: 0, email: '' 
}, 'user');
Array State
    No items
    const [items, setItems] = useTrackedState<string[]>([], 'items');
    setItems([...items, newItem]);

    State Timeline

    No updates yet

    Advanced Examples

    With Lazy Initialization

    function ExpensiveComponent() {
    // Expensive computation only runs once
    const [data, setData] = useTrackedState(
    () => expensiveComputation(),
    'expensiveData'
    );

    return <div>{data}</div>;
    }

    With Function Updates

    function TodoCounter() {
    const [count, setCount] = useTrackedState(0, 'todoCount');

    const increment = () => {
    // Function update ensures you always have latest value
    setCount(prev => prev + 1);
    };

    return <button onClick={increment}>Todos: {count}</button>;
    }

    Multiple State Variables

    function UserProfile() {
    const [name, setName] = useTrackedState('', 'userName');
    const [email, setEmail] = useTrackedState('', 'userEmail');
    const [age, setAge] = useTrackedState(0, 'userAge');

    return (
    <form>
    <input
    value={name}
    onChange={(e) => setName(e.target.value)}
    placeholder="Name"
    />
    <input
    value={email}
    onChange={(e) => setEmail(e.target.value)}
    placeholder="Email"
    />
    <input
    type="number"
    value={age}
    onChange={(e) => setAge(parseInt(e.target.value))}
    placeholder="Age"
    />
    </form>
    );
    }

    Complex Object State

    function Settings() {
    const [settings, setSettings] = useTrackedState(
    {
    theme: 'dark',
    notifications: true,
    language: 'en',
    },
    'appSettings'
    );

    const updateTheme = (theme: string) => {
    setSettings(prev => ({ ...prev, theme }));
    };

    const toggleNotifications = () => {
    setSettings(prev => ({
    ...prev,
    notifications: !prev.notifications,
    }));
    };

    return (
    <div>
    <select value={settings.theme} onChange={(e) => updateTheme(e.target.value)}>
    <option value="light">Light</option>
    <option value="dark">Dark</option>
    </select>
    <label>
    <input
    type="checkbox"
    checked={settings.notifications}
    onChange={toggleNotifications}
    />
    Enable Notifications
    </label>
    </div>
    );
    }

    Debugging Features

    Timeline View

    Every state change appears in the timeline with:

    • Timestamp
    • State label
    • Previous value
    • New value
    • Component that triggered the change

    Time Travel

    You can jump to any previous state:

    import { getTimeTravel } from 'react-dev-debugger';

    const timeTravel = getTimeTravel();
    timeTravel.stepBackward(); // Go to previous state
    timeTravel.stepForward(); // Go to next state

    Diff View

    See exactly what changed in complex state objects:

    // The debugger automatically shows:
    // - Added properties
    // - Removed properties
    // - Changed values

    Best Practices

    ✅ Do's

    • Use descriptive labels: useTrackedState(0, 'cartItemCount') instead of useTrackedState(0)
    • Keep state granular: Multiple small state variables are easier to track than one large object
    • Use function updates: When new state depends on old state

    ❌ Don'ts

    • Don't mutate state directly: Always use setState
    • Don't use generic labels: Avoid labels like 'state', 'data', 'value'
    • Don't over-nest: Deep nesting makes diffs harder to read

    Performance

    useTrackedState has minimal overhead:

    • Development: ~0.1ms per update
    • Production: Automatically disabled (zero overhead)

    TypeScript Support

    Full type inference and type safety:

    // Type is inferred
    const [count, setCount] = useTrackedState(0, 'counter');
    // count: number, setCount: (value: number | ((prev: number) => number)) => void

    // Explicit typing
    const [user, setUser] = useTrackedState<User | null>(null, 'currentUser');

    // With complex types
    interface Settings {
    theme: 'light' | 'dark';
    notifications: boolean;
    }

    const [settings, setSettings] = useTrackedState<Settings>(
    { theme: 'light', notifications: true },
    'settings'
    );

    Comparison with useState

    FeatureuseStateuseTrackedState
    API✅ Same✅ Same
    Performance✅ Fast✅ Fast (dev only)
    Debugging❌ None✅ Full timeline
    Time Travel❌ No✅ Yes
    Diff View❌ No✅ Yes
    Type Safety✅ Yes✅ Yes
    Production✅ Works✅ Auto-disabled

    Next Steps