useReducer in React: Simplify Complex State with Actions and a Reducer
What is useReducer?
useReducer is a React Hook that helps manage complex state when useState becomes hard to use. It works by handling state updates through actions and a reducer function.
Why Do We Need useReducer?
Sometimes, managing state in React using useState can get messy. Imagine you have a form with multiple fields, a shopping cart with items, or a game with health, shield, and score. Updating each piece of state separately can be confusing, and errors can occur.
- It organizes state updates in one place (the reducer function).
- It makes complex state predictable — you always know how state will change because it goes through the reducer.
- It simplifies code maintenance — instead of using many useState calls, you just dispatch action.
In other words, the flow is:(For More Understanding)
Let’s assume we have 500 rupees in our bank account.
If the user wants to withdraw or deposit money, they send the request usingdispatch(like giving a cheque or challan).
The reducer acts like a cashier: it processes the deposit or withdrawal and updates the balance.
The updated money becomes the new state, which React uses to update the UI.
useReducer Syntax:
const [state, dispatch] = useReducer(reducer, initialState);
Let me explain the scenario we’re implementing with useReducer: it’s a quiz app.
I’m going to explain whether this useEffect part will run correctly and how it works. Let’s break it down step by step.
useEffect(() => {
if (!startTime || examEnded) return;
const interval = setInterval(() => {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
if (elapsed >= EXAM_DURATION) {
dispatch({ type: "TICK", payload: EXAM_DURATION });
dispatch({ type: "END_EXAM" });
clearInterval(interval);
} else {
dispatch({ type: "TICK", payload: elapsed });
}
}, 1000);
return () => clearInterval(interval);
}, [startTime, examEnded]);if (!startTime || examEnded) return;
If any ONE of the two conditions is true, the interval should NOT run.
const elapsed = Math.floor((Date.now() - startTime) / 1000);Date.now() returns milliseconds.
- This number is not local time, it’s a global timestamp
- You can convert it to human-readable time depending on your timezone
startTime = 10:00:00.000 milliseconds it’s like 1707012000000 ms
current time = 10:00:14.000 milliseconds it’s like 1707012014000 ms
so (Date.now() — startTime) / 1000)
(1707012014000–1707012000000 /1000)
14000 ms now convert this ms into sec so divide by 1000
14000/1000 =14sec.
if (elapsed >= EXAM_DURATION) {
dispatch({ type: "TICK", payload: EXAM_DURATION });
dispatch({ type: "END_EXAM" });
clearInterval(interval);
} else {
dispatch({ type: "TICK", payload: elapsed });
}
}, 1000);if condition is true it run dispatch({ type: "TICK", payload: elapsed });
if false it run dispatch({ type: "TICK", payload: EXAM_DURATION }); dispatch({ type: "END_EXAM" });
This interval function runs every 1 second.
return () => clearInterval(interval);This is called a cleanup function in useEffect
Get Nesa sheeba J’s stories in your inbox
Join Medium for free to get updates from this writer.
Return in useEffect is used to clean up the interval, so it stops running when the component unmounts or when the effect runs again.
const handleAnswer = (option) => {
if (examEnded) return;
if (option === questions[currentQuestion].correctAnswer) {
dispatch({ type: "ANSWER_CORRECT" });
}
if (currentQuestion === questions.length - 1) {
dispatch({ type: "END_EXAM" });
return;
}
dispatch({ type: "NEXT_QUESTION" });
if (examEnded) return;
- If the condition is TRUE →
returnstops the function immediately. - If the condition is FALSE → function continues to the next line.
const quizReducer = (state, action) => {
switch (action.type) {
case "START_EXAM":
return {
...state,
startTime: Date.now(),
};
case "TICK":
return {
...state,
elapsedSeconds: action.payload,
};
case "ANSWER_CORRECT":
return {
...state,
score: state.score + 1,
};
case "NEXT_QUESTION":
return {
...state,
currentQuestion: state.currentQuestion + 1,
};
case "END_EXAM":
return {
...state,
examEnded: true,
};
case "RESTART":
return initialState;
default:
return state;
}
};i) case “START EXAM”
- when we dispatch {type:”START EXAM”}, this block will run.
- return {…state,startTime: Date.now(),};
- …state → copies the current state
- startTime: Date.now()→ sets the start time of the exam to the current timestamp in milliseconds.
ii)case “TICK”:
- when we dispatch {type:”TICK”}, this block will run.
- elapsedSeconds: action.payload.
- Updates the number of seconds that have passed since the exam started
- action.payload comes from the interval
iii) case “ANSWER_CORRECT”:
- when we dispatch {type:”TICK”}, this block will run.
- score:state.score +1
- Increments the score by 1.
- So every time the user answers correctly, their score increases.
iv)case “NEXT_QUESTION”:
- when we dispatch {type:”NEXT_QUESTION”}, this block will run.
- currentQuestion: state.currentQuestion + 1.
- Increments the current question index by 1.
- Moves the quiz to the next question in the array.
v)case “END_EXAM”:
- when we dispatch {type:”END_EXAM”}, this block will run.
- examEnded: true.
- Updates the state to mark that the exam has ended.
vi)case “RESTART”:
- return initialState.
const initialState = {
currentQuestion: 0,
score: 0,
startTime: null,
elapsedSeconds: 0,
examEnded: false,
};This is how we manage complex state in React using useReducer.
The useReducer hook is a powerful tool for managing complex state in React. Unlike useState, which works well for simple values, useReducer lets you organize state logic in one place using a reducer function and actions. It allows us to handle multiple state updates in an easy-to-understand way.
If you want to see the full UI code, I’ve attached the GitHub URL below. Feel free to go and check it out!








