Skip to Content
All memories

Beginners guide to useReducer

 — #React#Context#Hooks#Reducers

react useReducer guide

Intro

If you decide to not use redux in your small-scaled project, and figured that context might be enough for your state management needs, you might as well check out the useReducer hook. It brings the action-reducer pattern from redux to context which helps us separate rendering and state management. This separation results in components that are easier to maintain.

Concept

Before talking about the concept of useReducer hook, let’s recap how the reduce method in javascript works.The reduce() method receives a callback and the initial value as arguments and reduces all the elements in an array into a single output value which can be of any type (number, string, object, etc.).

A simple example of reduce method would be:

const result = baseArray.reduce((accumulator, currentValue) => {
   // functions body
   return resultValue;
}, initialValue);

Now that we know what reduce method does, let’s talk about when should we use useReducer hook in our project.

The first use case is when we have complex state structure, like nested arrays or objects. In this case, you can provide all the logic inside a single reducer which separates state management from rendering logic and makes it easier to manage.

Another use case is when you need to operate multiple state transitions on one state object. The useReducer hook can give you more predictable state transitions which helps you when complex state transitions are all happening at once.

The useReducer hook accepts a reducer function and an initial state as arguments, and returns the current state and a dispatch function:

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

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.

function reducer(state, action) {
 let newState;
 switch (action.type) {
   case '...':
     ...
     break;
 }
 return newState;
}

The initial state is the value the state is initialized with.

The dispatch function should be called every time we need to update the state. It takes an action object as an argument.

An action object is an object that describes how to update the state. If the action needs to have payload (information) then additional properties can be added to the action object.

const action = {
 type: 'remove',
 data: {
   key: 'value',
 }
};

Implementation

In order to understand how useReducer works, let’s build a simple to-do app using it.

First we need to declare out initial state:

import { useReducer, useState } from "react";
import "./styles.css";

const initialState = [
  {
    name: "Workout"
  },
  {
    name: "Study"
  },
  {
    name: "Sleep"
  }
];


const App = () => {

  return (
    <div className="todo-container">
      
    </div>
  );
};

export default App;

Then we create a todoItem component for our todo-list items, which displays the name and a remove button for deleting that particular item:

const TodoItem = ({ item }) => {
  const removeTodo = () => {

  };

  return (
    <div className="todo-item">
      <div className="name">{item.name}</div>
      <div className="delete" onClick={() => removeTodo()}>
        - remove
      </div>
    </div>
  );
};

Next, we need to add todo-items . So let’s add an input for todo name alongside a button for submitting the item:

import { useReducer, useState } from "react";
import "./styles.css";

const initialState = [...]

const App = () => {
  const [addInput, setAddInput] = useState("");

  const addTodo = () => {

  }

  return (
    <div className="todo-container">
      <div className="add-todo">
        <input
          placeholder="What are you going to do?"
          value={addInput}
          onChange={(e) => setAddInput(e.target.value)}
        />
        <button onClick={() => addTodo()}>+ Add</button>
      </div>
    </div>
  );
};

const TodoItem = ({ item }) => {...}

export default App;

Now we need to implement the functionality of adding and removing to the list. First let’s declare our useReducer with the initial state and write a reducer function with two actions, add and remove. This returns a state which we should map through in render and a dispatch method for dispatching the actions we need:

import { useReducer, useState } from "react";
import "./styles.css";

const initialState = [...]

const reducer = (state, action) => {  let newState;  switch (action.type) {    case "add":      newState = [...state, action.payload];      break;    case "remove":      newState = [...state.filter((item) => item.name !== action.payload.name)];      break;    default:      throw new Error();  }  return newState;};
const App = () => {
  const [addInput, setAddInput] = useState("");
  
  const [state, dispatch] = useReducer(reducer, initialState);
  const addTodo = () => {

  }

  return (
    <div className="todo-container">
      <div className="add-todo">
        <input
          placeholder="What are you going to do?"
          value={addInput}
          onChange={(e) => setAddInput(e.target.value)}
        />
        <button onClick={() => addTodo()}>+ Add</button>
      </div>
      
      <div className="todo-items">        {state.map((item, index) => (          <TodoItem key={index} item={item} dispatch={dispatch} />        ))}      </div>    </div>
  );
};

const TodoItem = ({ item }) => {...}

export default App;

In the end we need to use dispatch our actions in addTodo() and removeTodo() methods in order to change the state:

  const addTodo = () => {
    if (!Boolean(addInput)) {
      alert("Name cannot be empty...!");
      return;
    }
    dispatch({
      type: "add",
      payload: {
        name: addInput
      }
    });
    setAddInput("");
  };
  
  const removeTodo = () => {
    dispatch({
      type: "remove",
      payload: item
    });
  };

Conclusion

See the example in codesandbox

References

React Docs

Kent C. Dodds Article