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​