Table of Contents
- Introduction
- When to Use useState
- When to Use useReducer
- Example 1: Counter App with useState
- Example 2: Counter App with useReducer
- Example 3: Form Input Handling with useReducer
- Example 4: Building a quiz app with useReducer
- Comparison Between useState and useReducer
- Conclusion
Introduction
React offers two key hooks for managing state: useState and useReducer. While both are designed to handle state in functional components, they are used in different scenarios. This article explores the differences between the two and highlights when you should use each, with examples for better understanding
When to Use useState
useState is a simple and effective hook for handling local state when:
- You have simple state to manage (like booleans, numbers, or strings).
- You want direct updates to state with minimal setup.
- The state does not have complex transitions or dependencies on multiple variables.
Basic Syntax
const [state, setState] = useState(initialState);
- state: The current state.
- setState: A function to update the state.
- initialState:The initial state
When to Use useReducer
useReducer is useful when:
- You have complex state logic.
- Multiple state updates depend on one another.
Basic Syntax
const [state, dispatch] = useReducer(reducer, initialState);
- state: The current state.
- dispatch: A function to send an action to the reducer to trigger a state update.
- reducer: A reducer is a pure function that takes two arguments: the current state and an action. It returns the new state based on the action.
Basic Syntax
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
}
Action: An action is an object that describes what change should happen
It typically has a type property and optionally a payload.
The type tells the reducer what kind of state change to make.
The payload carries any additional data needed for the change.InitialState:The initial state ,just like initialstate in useState.
Example 1 counter app with useState
import React, { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
Explanation
- We use useState to track the count value.
- We have two buttons: one to increment and one to decrement the count state.
- The state is updated directly using the setCount function.
Example 2: Counter App with useReducer
import React, { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
}
Explanation
- The reducer function controls how the state should change based on the action dispatched.
- Instead of directly setting the state, we dispatch actions (increment, decrement) to trigger changes.
Example 3: Form Input Handling with useReducer
Let’s expand the concept to handling a form with multiple input fields. This scenario is ideal for useReducer since it updates multiple state properties based on actions.
import React, { useReducer } from 'react';
const initialState = {
name: '',
email: ''
};
function reducer(state, action) {
switch (action.type) {
case 'setName':
return { ...state, name: action.payload };
case 'setEmail':
return { ...state, email: action.payload };
default:
return state;
}
}
export default function Form() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<input
type="text"
value={state.name}
onChange={(e) => dispatch({ type: 'setName', payload: e.target.value })}
placeholder="Name"
/>
<input
type="email"
value={state.email}
onChange={(e) => dispatch({ type: 'setEmail', payload: e.target.value })}
placeholder="Email"
/>
<p>Name: {state.name}</p>
<p>Email: {state.email}</p>
</div>
);
}
Explanation
- The reducer manages the form state by updating different properties (name, email) based on the action’s type.
- Dispatch sends the action to the reducer to update the state. The payload carries the data (e.g., the input value).
Example 4: Building a quiz app with useReducer
Note: styling was done with tailwindcss
import React, { useReducer } from 'react';
// Quiz data with detailed explanations
const quizData = [
{
question: "What hook is used to handle complex state logic in React?",
options: ["useState", "useReducer", "useEffect", "useContext"],
correct: 1,
explanation: "useReducer is specifically designed for complex state management scenarios."
},
{
question: "Which function updates the state in useReducer?",
options: ["setState", "dispatch", "update", "setReducer"],
correct: 1,
explanation: "dispatch is the function provided by useReducer to trigger state updates."
},
{
question: "What pattern is useReducer based on?",
options: ["Observer Pattern", "Redux Pattern", "Factory Pattern", "Module Pattern"],
correct: 1,
explanation: "useReducer is inspired by Redux's state management pattern."
}
];
// Initial state with feedback state added
const initialState = {
currentQuestion: 0,
score: 0,
showScore: false,
selectedOption: null,
showFeedback: false, // New state for showing answer feedback
};
// Enhanced reducer with feedback handling
const reducer = (state, action) => {
switch (action.type) {
case 'SELECT_OPTION':
return {
...state,
selectedOption: action.payload,
showFeedback: true, // Show feedback when option is selected
};
case 'NEXT_QUESTION':
const isCorrect = action.payload === quizData[state.currentQuestion].correct;
const nextQuestion = state.currentQuestion + 1;
return {
...state,
score: isCorrect ? state.score + 1 : state.score,
currentQuestion: nextQuestion,
showScore: nextQuestion === quizData.length,
selectedOption: null,
showFeedback: false, // Reset feedback for next question
};
case 'RESTART':
return initialState;
default:
return state;
}
};
const Quiz = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { currentQuestion, score, showScore, selectedOption, showFeedback } = state;
const handleOptionClick = (optionIndex) => {
dispatch({ type: 'SELECT_OPTION', payload: optionIndex });
};
const handleNext = () => {
if (selectedOption !== null) {
dispatch({ type: 'NEXT_QUESTION', payload: selectedOption });
}
};
const handleRestart = () => {
dispatch({ type: 'RESTART' });
};
if (showScore) {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100 p-4">
<div className="bg-white rounded-lg shadow-lg p-8 max-w-md w-full">
<h2 className="text-2xl font-bold text-center mb-4">Quiz Complete!</h2>
<p className="text-xl text-center mb-6">
Your score: {score} out of {quizData.length}
</p>
<button
onClick={handleRestart}
className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition-colors"
>
Restart Quiz
</button>
</div>
</div>
);
}
const currentQuizData = quizData[currentQuestion];
const isCorrectAnswer = (optionIndex) => optionIndex === currentQuizData.correct;
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100 p-4">
<div className="bg-white rounded-lg shadow-lg p-8 max-w-md w-full">
<div className="mb-6">
<p className="text-sm text-gray-500 mb-2">
Question {currentQuestion + 1}/{quizData.length}
</p>
<h2 className="text-xl font-semibold mb-4">{currentQuizData.question}</h2>
</div>
<div className="space-y-3 mb-6">
{currentQuizData.options.map((option, index) => {
let buttonStyle = 'bg-gray-50 hover:bg-gray-100';
if (showFeedback && selectedOption === index) {
buttonStyle = isCorrectAnswer(index)
? 'bg-green-100 border-2 border-green-500 text-green-700'
: 'bg-red-100 border-2 border-red-500 text-red-700';
}
return (
<button
key={index}
onClick={() => handleOptionClick(index)}
disabled={showFeedback}
className={`w-full p-3 text-left rounded-lg transition-colors ${buttonStyle}`}
>
{option}
</button>
);
})}
</div>
{showFeedback && (
<div className={`p-4 rounded-lg mb-4 ${
isCorrectAnswer(selectedOption)
? 'bg-green-50 text-green-800'
: 'bg-red-50 text-red-800'
}`}>
{isCorrectAnswer(selectedOption)
? "Correct! "
: `Incorrect. The correct answer was: ${currentQuizData.options[currentQuizData.correct]}. `}
{currentQuizData.explanation}
</div>
)}
<button
onClick={handleNext}
disabled={!showFeedback}
className={`w-full py-2 px-4 rounded transition-colors ${
!showFeedback
? 'bg-gray-300 cursor-not-allowed'
: 'bg-blue-500 text-white hover:bg-blue-600'
}`}
>
Next Question
</button>
</div>
</div>
);
};
export default Quiz;
Explanation
*initial state with useReducer
// Initial state
const initialState = {
currentQuestion: 0,
score: 0,
showScore: false,
selectedOption: null,
showFeedback: false, // New state for feedback
};
- Reducer Function
const reducer = (state, action) => {
switch (action.type) {
case 'SELECT_OPTION':
return {
...state,
selectedOption: action.payload,
showFeedback: true, // Show feedback immediately
};
case 'NEXT_QUESTION':
const isCorrect = action.payload === quizData[state.currentQuestion].correct;
// ... rest of the logic
The reducer handles three actions:
- SELECT_OPTION: When user selects an answer
- NEXT_QUESTION: When moving to the next question
- RESTART: When restarting the quiz
Styling Logic
let buttonStyle = 'bg-gray-50 hover:bg-gray-100';
if (showFeedback && selectedOption === index) {
buttonStyle = isCorrectAnswer(index)
? 'bg-green-100 border-2 border-green-500 text-green-700'
: 'bg-red-100 border-2 border-red-500 text-red-700';
}
This code determines the button styling:
- Default: Gray background
- Correct answer: Green background with green border
- Wrong answer: Red background with red border
Feedback Display
{showFeedback && (
<div className={`p-4 rounded-lg mb-4 ${
isCorrectAnswer(selectedOption)
? 'bg-green-50 text-green-800'
: 'bg-red-50 text-red-800'
}`}>
{isCorrectAnswer(selectedOption)
? "Correct! "
: `Incorrect. The correct answer was: ${currentQuizData.options[currentQuizData.correct]}. `}
{currentQuizData.explanation}
</div>
)}
This shows feedback after an answer is selected:
*Displays whether the answer was correct or incorrect
*Shows the correct answer if wrong
*Includes an explanation
Hosted link of the quiz app
Comparison Between useState and useReducer
Feature | useState | useReducer |
---|---|---|
Best for | Simple state | Complex state logic |
State Management | Direct, using setState
|
Managed through a reducer function |
Boilerplate Code | Minimal | Requires more setup |
State Update | Inline with setState
|
Managed by dispatch and reducer
|
Conclusion
Both useState and useReducer are powerful hooks for managing state in functional components. useState is best suited for simple state, while useReducer shines when handling more complex scenarios where state updates are closely related. Choosing the right one depends on the complexity of the state you need to manage.