useReducer & Complex State Logic
5 questions foundWhat is the basic structure of a reducer function used with the useReducer hook?
Intermediate A reducer is a plain function that takes the current state and an action describing what happened, and returns the new state based on that action. It should never directly change the existing state, but instead always return a brand new state object reflecting the update.
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 });
Real-world example A counter component uses a simple reducer with increment and decrement actions, keeping all the logic for how the count changes clearly organized in one single function instead of scattered across multiple event handlers.
Common follow-ups: What happens if a reducer receives an action type it does not recognize?;Why should a reducer always return a brand new object instead of modifying the existing state directly?
Hooks;State Management
How would you manage a multi step form's state using useReducer instead of several separate useState calls?
Intermediate A multi step form often has many related pieces of state, like the current step number and each field's value, that change together in predictable ways. useReducer lets you organize all of these updates through clearly named actions, like next step or update field, keeping the related logic together instead of spread across many separate state variables.
function formReducer(state, action) {
switch (action.type) {
case 'updateField':
return { ...state, [action.field]: action.value };
case 'nextStep':
return { ...state, step: state.step + 1 };
default:
return state;
}
}
const [formState, dispatch] = useReducer(formReducer, { step: 1, name: '', email: '' });
Real-world example A job application form with several steps keeps all of its related state and update logic organized in one reducer, making it much easier to add a new field or step later without touching several separate useState calls scattered throughout the component.
Common follow-ups: How would you validate each step's data before allowing the user to move to the next step?;At what point does a form become complex enough to justify switching from useState to useReducer?
Forms: Controlled & Uncontrolled Components;Design Patterns in React
How would you combine useReducer with the Context API to share complex state across many components?
Advanced You create a reducer to manage the shared state and its update logic, then use Context to make both the current state and the dispatch function available to any component nested inside, without needing to pass them down manually as props through every layer.
const CartContext = React.createContext();
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
return (
<CartContext.Provider value={{ state, dispatch }}>
{children}
</CartContext.Provider>
);
}
function useCart() {
return useContext(CartContext);
}
Real-world example A shopping app shares its cart state and update logic across many unrelated components, like the header badge and the checkout page, using a single reducer paired with Context, avoiding the need for prop drilling entirely.
Common follow-ups: How is this combination similar to how a library like Redux works internally?;What happens to performance if this shared context updates very frequently?
Context API;State Management Libraries (Redux Zustand & Recoil)
How do you pass extra data along with an action when using useReducer?
Intermediate An action object can include any additional properties beyond just its type, commonly called a payload, letting the reducer know exactly what data it needs to correctly compute the new state for that specific action.
function reducer(state, action) {
switch (action.type) {
case 'addItem':
return { items: [...state.items, action.payload] };
default:
return state;
}
}
dispatch({ type: 'addItem', payload: { id: 1, name: 'Apple' } });
Real-world example A shopping cart reducer receives the specific item being added as a payload inside the dispatched action, letting it correctly add exactly that item to the existing list of cart items.
Common follow-ups: Is there a required naming convention for the extra data included in an action?;How would you dispatch multiple different pieces of data together in a single action?
State Management;Design Patterns in React
When should you choose useReducer over useState for managing a component's state?
Beginner useReducer becomes a better fit when a component has several related pieces of state that often update together, or when the logic for updating state grows complicated enough that keeping it in one clearly organized function, rather than scattered across many event handlers, makes the code easier to follow.
// Simple, independent value, useState is enough
const [isOpen, setIsOpen] = useState(false);
// Several related values updating together, useReducer fits better
const [state, dispatch] = useReducer(formReducer, { name: '', email: '', errors: {} });
Real-world example A simple toggle switch component keeps using useState since it only tracks one independent value, while a complex settings form with many interdependent fields switches to useReducer to keep its update logic organized in one place.
Common follow-ups: Is there ever a real performance difference between choosing useState versus useReducer?;Can a component use both useState and useReducer together for different pieces of its state?
Hooks;Forms: Controlled & Uncontrolled Components