DEV Community

Cover image for useReducer and how it is different from useState
Adaobi Okwuosa
Adaobi Okwuosa

Posted on

useReducer and how it is different from useState

Table of Contents

  1. Introduction
  2. When to Use useState
  3. When to Use useReducer
  4. Example 1: Counter App with useState
  5. Example 2: Counter App with useReducer
  6. Example 3: Form Input Handling with useReducer
  7. Example 4: Building a quiz app with useReducer
  8. Comparison Between useState and useReducer
  9. 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);
Enter fullscreen mode Exit fullscreen mode
  • 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);

Enter fullscreen mode Exit fullscreen mode
  • 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;
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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>
  );
}


Enter fullscreen mode Exit fullscreen mode

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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>
  );
}




Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

Explanation

*initial state with useReducer

// Initial state
const initialState = {
  currentQuestion: 0,
  score: 0,
  showScore: false,
  selectedOption: null,
  showFeedback: false, // New state for feedback
};
Enter fullscreen mode Exit fullscreen mode
  • 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

Enter fullscreen mode Exit fullscreen mode

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';
}
Enter fullscreen mode Exit fullscreen mode

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>
)}

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)