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