Learn React hooks Part 2 - useReducer

In the last tutorial we primarily covered useState and useEffect Hooks, In this tutorial we're going to cover the useReducer hook.

  • useReducer

If you're working with react you should have heard about redux, the popular framework for managing state for react apps, useReducer is built on the same principle, for the uninitiated, reducer is simply a function which takes an action as an input, performs the action on our state and returns the new state

Here's the different ways you can use useReducer

// initialState is the intial state of our component,
// dispatch is a function which we can use to `dispatch`  an action
import { useReducer } from "react";
// simple example
const [state, dispatch] = useReducer(initialState);

// init is a function which returns the initial state
// initialArgs -> the arguments which are passed to the init
// reducer is a function which accepts an action and returns new state 
// (resulted from performing the aciton on the current state)  
const [state, dispatch] = useReducer(reducer, initialArgs, init);

// This is the another way, i personally prefer this over the others 

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

Now that we know the basic usage lets update the Typing component which we created in the previous tutorial to work with useReducer

Here's the component without the reducer, in case you've not seen the previous tutorial

import React, { useState, useEffect } from "react";

function TypingComponent({ textToType, delay, loop }) {
  const [text, setText] = useState("");
  const [currentIndex, setCurrentIndex] = useState(0);
  useEffect(() => {
    if (currentIndex < textToType.length) {
      setTimeout(() => {
        setText(text + textToType[currentIndex]);
        setCurrentIndex(currentIndex + 1);
      }, delay);
    } else if (loop) {
      // reset the text and the index
      setText("");
      setCurrentIndex(0);
    }
  }, [currentIndex]);
  return <div>{text}</div>;
}
function App() {
  return (
    <div className="App">
      <TypingComponent textToType="Thank you..." delay={300} loop={true} />
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

if you've understood the concept of reducer, you can clearly see that we can dispatch the actions wherever we're calling setters, So lets get rid of useState and its setters(commented) first lets create our reducer

import React, { useReducer, useEffect } from "react";


function TypingComponent({ textToType, delay, loop }) {
// const [text, setText] = useState("");
// const [currentIndex, setCurrentIndex] = useState(0);

// reducer function accepts state and action, returns new state
const reducer = (state, action ) => {}

// initial state of our component
const initialState = {text: "", currentIndex: 0}

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

  useEffect(() => {
    if (state.currentIndex < textToType.length) {
      setTimeout(() => {
        // setText(text + textToType[currentIndex]);
        // setCurrentIndex(currentIndex + 1);
      }, delay);
    } else if (loop) {
      // reset the text and the index
        // setText("");
        // setCurrentIndex(0);
    }
  }, [state.currentIndex]);
  return <div>{state.text}</div>;
}

now lets add some actions and update our reducer function, before that lets see what could be the possible actions, In the above commented code all the setters can be considered as actions since they're update our state typically action is an object which should be uniquely identified by the reducer, it can have the data or any other keys neccessary for letting the reducer perform updates on state

So the action for updating the text can be like

  const action = {
    type: "APPEND_LETTER",
    letter: nextLetter
  }

this action object needs to be dispatched using the dispatch method, so we can have 4 different actions for our app, as written below,


// initial state of our component
const initialState = {text: "", currentIndex: 0}

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

  useEffect(() => {
    if (state.currentIndex < textToType.length) {
      setTimeout(() => {
        dispatch({ type: "APPEND_LETTER", letter: textToType[state.currentIndex] });
        dispatch({ type: "UPDATE_CURRENT_INDEX" });
      }, delay);
    } else if (loop) {
      // reset the text and the index

      dispatch({ type: "RESET_TEXT" });
      dispatch({ type: "RESET_CURRENT_INDEX" });

    }
  }, [state.currentIndex]);
  return <div>{state.text}</div>;
}

Now that we have specified the actions, lets handle them in the reducer We'll be using a switch statement to identify the type and based on the matching case will perform neccessary steps Like below


// reducer function accepts state and action, returns new state
const reducer = (state, action ) => {
  switch (action.type) {
    case "APPEND_LETTER":
      return { ...state, text: `${state.text}${action.letter}` };
    case "RESET_TEXT":
      return { ...state, text: "" };

    case "UPDATE_CURRENT_INDEX":
      return { ...state, currentIndex: state.currentIndex + 1 };
    case "RESET_CURRENT_INDEX":
      return { ...state, currentIndex: 0 };

    default:
      return state;
  }

}

Note

When you're returning the state from the reducer it must have a different reference, that is it should not mutate the exisiting state so that react can update the component when the state changes


Here's the updated code

function reducer(state, action) {
  switch (action.type) {
    case "APPEND_LETTER":
      return { ...state, text: `${state.text}${action.letter}` };
    case "RESET_TEXT":
      return { ...state, text: "" };

    case "UPDATE_CURRENT_INDEX":
      return { ...state, currentIndex: state.currentIndex + 1 };
    case "RESET_CURRENT_INDEX":
      return { ...state, currentIndex: 0 };

    default:
      return state;
  }
}
function Typer({ textToType, delay, loop }) {
  const [state, dispatch] = useReducer(reducer, { currentIndex: 0, text: "" });
  useEffect(() => {
    if (state.currentIndex < textToType.length) {
      setTimeout(() => {
        dispatch({
          type: "APPEND_LETTER",
          letter: textToType[state.currentIndex]
        });
        dispatch({ type: "UPDATE_CURRENT_INDEX" });
      }, delay);
    } else if (loop) {
      dispatch({ type: "RESET_TEXT" });

      dispatch({ type: "RESET_CURRENT_INDEX" });
    }
  }, [state.currentIndex]);
  return <div className="App">{state.text}</div>;
}

You can take a look at the full working code here Codesandbox