React Hook: useReducer

useReducer

Hook useReducer is like useState, that it manages states and rerender component whenever the state changes. useReducer gives more concrete way to handle more complex function. useReducer has very similar pattern and use with redux, but it takes away a lot of boiler plate from redux.

The coding is more complex , but it gives a lot of control to the state. Changes can occur only in listed type of actions.


Using useReducer

Example: CountApp.js

import React, { useReducer } from "react";

const ACTIONS = {
    INCREMENT: "increment",
    DECREMENT: "decrement"
}

function reducer(state, action) {
  switch (action.type) {
    case ACTIONS.INCREMENT:
      return { count: state.count + 1 };
    case ACTIONS.DECREMENT:
      return { count: state.count - 1 };
    default:
      return state;
  }
}

function CountApp() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  function increment() {
    dispatch({ type: ACTIONS.INCREMENT });
  }

  function decrement() {
    dispatch({ type: ACTIONS.DECREMENT });
  }

  return (
    <div>
      <button onClick={decrement}>-</button>
      {state.count}
      <button onClick={increment}>+</button>
    </div>
  );
}

export default CountApp;

Breakdown

Hook useReducer

const [state, dispatch] = useReducer(reducer, { count: 0 })

Hook useReducer takes 2 parameters:

Hook useReducer returns 2 values:

Function reducer

function reducer(state, action) {
    ... switch cases...
}

Function reducer takes 2 parameters:

Function reducer codes:

function reducer(state, action) {
  switch (action.type) {
    case ACTIONS.INCREMENT:
      return { count: state.count + 1 };
    case ACTIONS.DECREMENT:
      return { count: state.count - 1 };
    default:
      return state;
  }
}
function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      return state;
  }
}

Action types

In our example, there are 2 actions: "increment" and "decrement". We can leave the variable name as strings like in the useState example. However for the typical and more complex use, rather than hardcode the variable here and there, action types should be organized tidily in an object. This will make it very clear what our options are, nothing mysterious, and not prone to typing error.

const ACTIONS = {
    INCREMENT: "increment",
    DECREMENT: "decrement"
}

Dispatch function

This hook is typically used to handle more than 1 type of action. When we call the dispatch function, what we call it with, will be set as action variable for function reducer. Current state will be used as state variable, and dispatch function will return new current state.

function increment() {
    dispatch({ type: ACTIONS.INCREMENT });
}
function increment() {
    dispatch({ type: "increment" });
}

Comparison with useState

function IncrementUseState() {
  const [number, setNumber] = useState(0);
    
  function increment() {
    setNumber((prevNumber) => prevNumber + 1);
  }
    
  function decrement() {
    setNumber((prevNumber) => prevNumber - 1);
  }
    
  return (
    <div>
      <button onClick={decrement}>-</button>
      {number}
      <button onClick={increment}>+</button>
    </div>
  );
}

Todo App

Input box

First, we are creating an input box that's capturing user's typing. Using useState hook, the text typed in the box is kept as a state, initialized with "". Input box's onChange is used to detect each keystroke, and setState function update the state dinamically, each keystrokeas change detected. At this stage, the text are only prepared, but not stored yet, in the actually todo list.

function App() {
  const [task, setTask] = useState("");
  ...
  return (
    <form ...>
      <input 
        type="text"
        value={task}
        onChange={(e) => setTask(e.target.value)}
      />
    </form>
  );
}

Submit form

Place onSubmit functionality in the form element. Pressing enter upon typing in the input box triggers this. Say the onSubmit function name is handleSubmit.

function App() {
  ...
  function handleSubmit(e) {
    e.preventDefault();
    dispatch({ type: ACTIONS.ADD_TODO, payload: {item: item} });
    setItem("");
  }
  return (
    <form onSubmit={handleSubmit}>
      <input ... />
    </form>
  );
}

Create and manage todo list with useReducer

Use useReducer to create and manage todos, the array of the task list.

const ACTIONS = {
    ADD_TODO: "add-todo",
    ...
}

function reducer(todos, action) {
    switch(action.type) {
        case ACTIONS.ADD_TODO:
            return ...;
        default:
            return todos;
    }
}

function App(){
    const [todos, dispatch] = useReducer(reducer, [])
    ...
    return (
        ...
    )
}

Action 1: Add new task to the todo list

Make case 1 of the reducer action type functioning. In this case, return an array with:

function reducer(todos, action) {
    switch(action.type) {
        case ACTIONS.ADD_TODO:
            return [...todos, createTask(action.payload.task)];
        ...
    }
}

function createTask(task) {
    return {
        id: Date.now(),
        task: task,
        complete: false
    }
} 

Display the todo list

In most simple way, we can display the list of task items.

function App() {
    ...
    return (
        <div>
            ...
            <ul>
                {todos.map(e => {
                    return <li>{e.task}</li>
                })}
            </ul>
        </div>
    )
}