Skip to main content

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​

FeatureuseReduceruseTrackedReducer
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​