useTrackedReducer
Drop-in replacement for useReducer with automatic action and state tracking.
Overview
useTrackedReducer works exactly like React's useReducer but tracks every action dispatch and resulting state changes in the debugger timeline.
API
const [state, dispatch] = useTrackedReducer<S, A>(
reducer: (state: S, action: A) => S,
initialState: S,
label?: string
);
Parameters
- reducer: Reducer function
(state, action) => newState - initialState: Initial state value
- label (optional): Human-readable label for debugging
Returns
Returns the current state and a dispatch function.
Basic Example
import { useTrackedReducer } from 'react-dev-debugger';
type State = { count: number };
type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useTrackedReducer(
reducer,
{ count: 0 },
'counter'
);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}
Live Playground
Shopping Cart Example
Add Items to Cart
const [cart, dispatch] = useTrackedReducer(
cartReducer,
{ items: [], total: 0 },
'shoppingCart'
);
dispatch({
type: 'ADD_ITEM',
payload: { id: 1, name: 'Book', price: 29.99 }
});Cart Contents
Cart is empty
Reducer Function
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
// Add or increment quantity
return { ...state, items: [...], total: ... };
case 'REMOVE_ITEM':
// Remove item from cart
return { ...state, items: [...] };
case 'CLEAR_CART':
return { items: [], total: 0 };
default:
return state;
}
}Action Timeline
No actions dispatched yet
Advanced Examples
Shopping Cart
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: CartItem[];
total: number;
}
type CartAction =
| { type: 'ADD_ITEM'; payload: CartItem }
| { type: 'REMOVE_ITEM'; payload: number }
| { type: 'UPDATE_QUANTITY'; payload: { id: number; quantity: number } }
| { type: 'CLEAR_CART' };
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM': {
const existing = state.items.find(item => item.id === action.payload.id);
if (existing) {
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
),
total: state.total + action.payload.price,
};
}
return {
items: [...state.items, action.payload],
total: state.total + action.payload.price,
};
}
case 'REMOVE_ITEM': {
const item = state.items.find(i => i.id === action.payload);
return {
items: state.items.filter(i => i.id !== action.payload),
total: state.total - (item ? item.price * item.quantity : 0),
};
}
case 'UPDATE_QUANTITY': {
const item = state.items.find(i => i.id === action.payload.id);
if (!item) return state;
const diff = action.payload.quantity - item.quantity;
return {
...state,
items: state.items.map(i =>
i.id === action.payload.id
? { ...i, quantity: action.payload.quantity }
: i
),
total: state.total + (diff * item.price),
};
}
case 'CLEAR_CART':
return { items: [], total: 0 };
default:
return state;
}
}
function ShoppingCart() {
const [cart, dispatch] = useTrackedReducer(
cartReducer,
{ items: [], total: 0 },
'shoppingCart'
);
return (
<div>
<button onClick={() => dispatch({
type: 'ADD_ITEM',
payload: { id: 1, name: 'Book', price: 15, quantity: 1 }
})}>
Add Book
</button>
<p>Total: ${cart.total}</p>
</div>
);
}
Todo List with Async Actions
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoState {
todos: Todo[];
loading: boolean;
error: string | null;
}
type TodoAction =
| { type: 'ADD_TODO'; payload: string }
| { type: 'TOGGLE_TODO'; payload: number }
| { type: 'DELETE_TODO'; payload: number }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null }
| { type: 'FETCH_SUCCESS'; payload: Todo[] };
function todoReducer(state: TodoState, action: TodoAction): TodoState {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{ id: Date.now(), text: action.payload, completed: false }
],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
),
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload),
};
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload, loading: false };
case 'FETCH_SUCCESS':
return { ...state, todos: action.payload, loading: false };
default:
return state;
}
}
Form State Management
interface FormState {
values: Record<string, any>;
errors: Record<string, string>;
touched: Record<string, boolean>;
isSubmitting: boolean;
}
type FormAction =
| { type: 'SET_FIELD'; payload: { name: string; value: any } }
| { type: 'SET_ERROR'; payload: { name: string; error: string } }
| { type: 'SET_TOUCHED'; payload: string }
| { type: 'SET_SUBMITTING'; payload: boolean }
| { type: 'RESET_FORM' };
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
values: {
...state.values,
[action.payload.name]: action.payload.value,
},
};
case 'SET_ERROR':
return {
...state,
errors: {
...state.errors,
[action.payload.name]: action.payload.error,
},
};
case 'SET_TOUCHED':
return {
...state,
touched: { ...state.touched, [action.payload]: true },
};
case 'SET_SUBMITTING':
return { ...state, isSubmitting: action.payload };
case 'RESET_FORM':
return {
values: {},
errors: {},
touched: {},
isSubmitting: false,
};
default:
return state;
}
}
Debugging Features
Action Timeline
Every dispatched action appears in the timeline with:
- Action type
- Action payload
- Previous state
- New state
- Time elapsed
Action Replay
Replay actions to reproduce bugs:
import { getTimeline } from 'react-dev-debugger';
const timeline = getTimeline();
const actions = timeline.getEntries()
.filter(entry => entry.hookType === 'reducer')
.map(entry => entry.update);
// Replay all actions
actions.forEach(action => dispatch(action));
State Diffing
See exactly what changed after each action.
Best Practices
✅ Do's
- Use descriptive action types:
'ADD_TODO'instead of'add' - Keep reducers pure: No side effects in reducer functions
- Use TypeScript: Type your state and actions for safety
- Group related state: Use reducer when state has complex update logic
❌ Don'ts
- Don't mutate state: Always return new state objects
- Don't dispatch in render: Only dispatch in event handlers or effects
- Don't make API calls in reducers: Keep them in effects or middleware
TypeScript Support
Full type inference:
type State = { count: number };
type Action = { type: 'increment' } | { type: 'decrement' };
const [state, dispatch] = useTrackedReducer<State, Action>(
reducer,
{ count: 0 },
'counter'
);
// state: State
// dispatch: (action: Action) => void
Comparison with useReducer
| Feature | useReducer | useTrackedReducer |
|---|---|---|
| API | ✅ Same | ✅ Same |
| Performance | ✅ Fast | ✅ Fast (dev only) |
| Action Tracking | ❌ None | ✅ Full timeline |
| Time Travel | ❌ No | ✅ Yes |
| Replay Actions | ❌ No | ✅ Yes |
| Type Safety | ✅ Yes | ✅ Yes |
Next Steps
- useTrackedState - For simple state
- State Tracker - Understanding state tracking
- Timeline - View action history