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 ofuseTrackedState(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
| Feature | useState | useTrackedState |
|---|---|---|
| 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
- useTrackedReducer - For complex state logic
- useWhyDidYouUpdate - Find unnecessary re-renders
- Timeline - Understanding the timeline view