Hello Sunil
react-usereducer-hook-feature-image

React useReducer Hook – Manage App State Better

useReducer hook is an alternative to the useState hook, useReducer helps you manage complex state logic in React applications.

When combined with other hooks like useContext, useReducer can be a good alternative to Redux. In certain cases, it is an outright better option.

In this tutorial, we will explore the useReducer hook in depth, reviewing the scenarios in which you should and shouldn’t use it.

Let’s get started!

What is useReducer Hook?

useState hook is not the only hook to manage state, useReducer is also used for the same.

useReducer provides us with a mechanism to change a state based on the rules we provide, taking an initial state as input. That is simply useState dressed up.

useReducer hook is better than useState hook when we want to manage complex component states.

Syntax of useReducer Hook

The useReducer hook accepts 2 arguments: the reducer function and the initial state. The hook then returns an array of 2 items: the current state and the dispatch function.

import { useReducer } from 'react';

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, initialState);

  const action = {
    type: 'ActionType'
  };

  return (
    <button onClick={() => dispatch(action)}>
      Click me
    </button>
  );
}

Don’t worry about understanding this upfront, we will go through every inch of what this means and how it works.

useReducer Hook Syntax Breakdown

Now, let’s decipher what the terms of initial state, action object, dispatch, and reducer mean.

Initial State

initialState defines the initial state of the component. For example, in the case of a counter state, the initial value is:

// initial state
const initialState = { 
  counter: 0 
};

Let’s take one more example of initialState which contains a list of dogs up for adoption with their name, breed, and adoption status.

const initialState = [
 {
      name: "Waffles",
      breed: "Chihuahua",
      adopted: false,
  },
  {
      name: "Charlie",
      breed: "Pitbull",
      adopted: true,
  },
  {
      name: "Prince",
      breed: "German Shepherd",
      adopted: false,
  },
];

Action Object

An action object is an object that describes how to update the state.

Typically, the action object has a property type — a string describing what kind of state update the reducer must do.

For example, an action object to increase the counter can look as follows:

const action = {
  type: 'increase'
};

If the action object must carry some useful information (aka payload) to be used by the reducer, then you can add additional properties to the action object.

For example, here is an action object meant to add a new user to the users state array:

const action = {
  type: 'add',
  user: { 
    name: 'Sunil Pradhan',
    email: 'skp@mail.com'
  }
};

user is a property that holds the information about the user to add.

The action object is interpreted by the reducer function (described below).

Dispatch Function

Dispatch in React is simply a function that takes an instruction about what to do, then passes that instruction to the reducer as an “action”.

In simple terms, see dispatch as a friendly boss that likes to tell the reducer what to do and is happy about it.

The dispatch function is created for you by the useReducer hook:

const [state, dispatch] = useReducer(reducer, initialState);

So, dispatching means a request to update the state.

If you are familiar with Redux, this term dispatch might not be new to you.

Reducer Function

Reducer is a function that provides instructions on how to manage state. It takes two parameters state and action and it returns a new state.

// reducer type
(state, action) => newState

In other words, the reducer is a pure function that accepts 2 parameters: the current state and an action object.

Depending on the action object, the reducer function must update the state in an immutable manner, and return the new state.

The following reducer function supports the increase and decrease of a counter state:

function reducer(state, action) {
  let newState;
  switch (action.type) {
    case 'increase':
      newState = { counter: state.counter + 1 };
      break;
    case 'decrease':
      newState = { counter: state.counter - 1 };
      break;
    default:
      throw new Error();
  }
  return newState;
}

The reducer above doesn’t modify directly the current state in the state variable, but rather creates a new state object stored in newState, then returns it.

The function which contains all your state updates is called the reducer. This is because you are reducing the state logic into a separate function. The method you call to perform the operations is the dispatch method.

React checks the difference between the new and the current state to determine whether the state has been updated. So do not mutate the current state directly.

If all these terms sound too abstract, no worries! Let’s see how useReducer works in an interesting example.

Example 1 – Classic Counter

In the below code example we have used useReducer hook to implement classic counter functionality.

useReducer hook in the below code accepts two arguments: the reducer function and the initial state value.

SimpleCounter.jsx

import { useReducer } from 'react';

const initialState = 0;
function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      throw new Error();
  }
}

const SimpleCounter = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      <h1>
        Counter: {state}
        <br />
        <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
        <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
      </h1>
    </>
  );
};

export default SimpleCounter;

App.jsx

import SimpleCounter from './SimpleCounter';

function App() {
  return (
    <div>
      <SimpleCounter />
    </div>
  );
}

export default App;

The initial state is an integer value, which starts from 0. The reducer function takes in two arguments: the current state and action where the action is of string type in this example.

Note that we have used JSX in SimpleCounter.jsx to return the counter component having two buttons (+ and -) to increase and decrease the counter value.

On clicking these buttons in real-time, the dispatch function will be called which is usually used to change or replace the component’s state value.

We have also passed the action of type string i.e, type: 'INCREMENT' OR type: 'DECREMENT' in the dispatch function.

As we can observe in the output, we can dynamically click these increment and decrement buttons to increase and decrease the counter state value respectively.

Output:

usereducer-hook-example-1

Example 2 – Form Object

In the below code example, we have used useReducer hook to implement a form object with name and age as form fields.

useReducer hook in the below code accepts two arguments: the reducer function and the initial state value object.

FormObject.jsx

import { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'increment': {
      return {
        name: state.name,
        age: state.age + 1,
      };
    }
    case 'decrement': {
      return {
        name: state.name,
        age: state.age - 1,
      };
    }
    case 'change': {
      return {
        name: action.Name,
        age: state.age,
      };
    }
  }
  throw Error(action.type);
}

const FormObject = () => {
  const [state, dispatch] = useReducer(reducer, { name: 'Sunil', age: 21 });

  return (
    <>
      <input
        value={state.name}
        onChange={(e) => dispatch({ type: 'change', Name: e.target.value })}
      />
      <button onClick={() => dispatch({ type: 'increment' })}>
        Increment age
      </button>
      <button onClick={() => dispatch({ type: 'decrement' })}>
        Decrement age
      </button>
      <p>
        Age of {state.name} is {state.age}
      </p>
    </>
  );
};

export default FormObject;

App.jsx

import SimpleCounter from './FormObject';

function App() {
  return (
    <div>
      <FormObject />
    </div>
  );
}

export default App;

An initial state is an object with the name ‘Sunil’ and age as ’21’. The reducer function takes in two arguments: the current state and action where the action is of string type in this example.

Note that we have used JSX in FormObject.jsx to return the component. The component consists of an input box and increment & decrement age buttons.

On clicking these buttons in real-time, the dispatch function will be called which is usually used to change or replace the component’s state.

We have also passed an action of type string i.e, type: 'increment' OR type:'decrement' OR type:'change' in the dispatch function.

Note that what we type in the input box will be treated as a target value and it will be passed in the action of type: 'change'.

The action is passed to the reducer function as an argument where we have used switch statements for different action-type cases to change/replace and return the new component’s state value.

Output:

usereducer-hook-example-2

As we can observe in the output, we can dynamically click these increment and decrement buttons to increase and decrease the age state value respectively.

Also what we type in the input box, will be reflected as the name whose age is specified.

Ok, now let’s get to the next example and make this.

Example 3 – Add to Cart

On clicking Add to Cart button, items should be added in the cart, and Items in Cart and Total Price should increase. However, on clicking Remove from Cart button, the opposite should happen.

AddToCart.jsx

import { useReducer } from 'react';

const itemsInCart = [
  {
    id: 1,
    name: 'Butter Chicken',
    price: 200,
  },
  {
    id: 2,
    name: 'Naan',
    price: 40,
  },
  {
    id: 3,
    name: 'Jalebi',
    price: 20,
  },
  {
    id: 4,
    name: 'Gulab Jamun',
    price: 10,
  },
];

const reducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TO_CART':
      return {
        ...state,
        totalItems: state.totalItems + 1,
        totalPrice: state.totalPrice + action.payload,
      };
    case 'REMOVE_FROM_CART':
      return {
        ...state,
        totalItems: state.totalItems - 1,
        totalPrice: state.totalPrice - action.payload,
      };
    default:
      return { ...state };
  }
};

const AddToCart = () => {
  const [state, dispatch] = useReducer(reducer, {
    totalItems: 0,
    totalPrice: 0,
  });
  return (
    <div>
      <h2>Items in Cart: {state.totalItems}</h2>
      <h2>Total Price: {state.totalPrice}</h2>
      <hr />
      {itemsInCart.map(({ name, price }) => (
        <div>
          <h3>
            Product: {name} || Price: {price}
          </h3>
          <button
            onClick={() => dispatch({ type: 'ADD_TO_CART', payload: price })}
          >
            Add to Cart
          </button>
          <button
            onClick={() =>
              dispatch({ type: 'REMOVE_FROM_CART', payload: price })
            }
          >
            Remove from Cart
          </button>
        </div>
      ))}
    </div>
  );
};

export default AddToCart;

App.jsx

import AddToCart from './AddToCart';

function App() {
  return (
    <div>
      <AddToCart />
    </div>
  );
}

export default App;

Output:

usereducer-hook-example-3

Example 4 – Fetching Data with useReducer

Let’s say you are fetching some data, and you want to display:

  • loading… while it’s fetching
  • the data once you have it
  • or an error if there is one

You will want all three of these to be in sync with each other. If you get the data, you want to make sure it’s not loading and there’s no error. If you get an error, it’s not loading and there’s no data.

This is a good use case for useReducer hook.

DataFetch.jsx

import React, { useReducer, useEffect } from 'react';
import axios from 'axios';

const initialState = {
  loading: true,
  error: '',
  post: {},
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_SUCCESS':
      return {
        loading: false,
        post: action.payload,
        error: '',
      };
    case 'FETCH_ERROR':
      return {
        loading: false,
        post: {},
        error: 'Something went wrong!',
      };

    default:
      return state;
  }
};

const DataFetch = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    axios
      .get('https://jsonplaceholder.typicode.com/posts/1')
      .then((response) => {
        dispatch({ type: 'FETCH_SUCCESS', payload: response.data });
      })
      .catch((error) => {
        dispatch({ type: 'FETCH_ERROR' });
      });
  }, []);

  return (
    <div>
      {state.loading ? 'Loading' : state.post.title}
      {state.error ? error : null}
    </div>
  );
};

export default DataFetch;

App.jsx

import DataFetch from './DataFetch';


function App() {
  return (
    <div>
      <DataFetch />
    </div>
  );
}

export default App;

Here we have used useEffect hook and axios library along with useReducer hook.

useReducer Hook vs useState Hook

useReducer and useState both return a stateful value (state), and a function to update the state (setState and dispatch).

In addition, both hooks receive an initial state value (initialValue).

The main difference in these two initializations is that useReducer also takes a reducer function, which will be called when we use the returned dispatch function.

// useState implementation
const [state, setState] = useState(initialState);

// useReducer implementation
const [state, dispatch] = useReducer(reducer, initialState);

Conclusion

The useState hook is implemented using useReducer hook. It means that useReducer is primitive, and you can use it for everything that you can do with useState.

  • useReducer hook is extremely useful when working on complex and different states depend on each other.
  • useReducer is very similar to Redux if you want to not use a 3rd-party library or if it’s only for a component or two.

Hope this article helped you to understand useReducer hook what is exactly.

Further Reading

How useful was this post?

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

We are sorry that this post was not useful for you!

Let us improve this post!

Tell us how we can improve this post?

Similar articles you may like

Sunil Pradhan

Hi there 👋 I am a front-end developer passionate about cutting-edge, semantic, pixel-perfect design. Writing helps me to understand things better.

Add comment

Stay Updated

Want to be notified when our article is published? Enter your email address below to be the first to know.

Sunil Pradhan

Hi there 👋 I am a front-end developer passionate about cutting-edge, semantic, pixel-perfect design. Writing helps me to understand things better.