Mastering React Hooks: Fundamentals for Beginners

React Hooks are a new feature in React 16.8 that allows you to use state and other React features without writing classes. Let's explore further here!

Mastering React Hooks

Anyone who has ever worked with React must have occasionally wondered if it makes sense to use a stateful or stateless (functional) component. You will struggle greatly with LifeCycle if you use stateful components. Thankfully, React developers quickly identified this issue and released a new feature called React hooks. Let's have a deep dive into React Hooks via this article!

>> Read more about React:

Understanding React Hooks

React Hooks are a new feature in React 16.8 that allows you to use state and other React features without writing classes.

Class components, while necessary for accessing state, can be complex and difficult to comprehend. However, hooks provide a solution to this issue. They are functions that allow us to ‘hook’ into state and various lifecycle methods. With hooks, we can incorporate state into function components, thereby eliminating the constant need for class components.

Instead of setting state in a constructor() using this.state = {name: "initial"}, we can call the React function useState with the syntax const [name, setName] = React.useState("initial"). The first value is the state we wish to monitor, and the second value is a function to modify only that piece of state, replacing the need for this.setState calls.

As we no longer extend a React.Component, we use another hook, useEffect(), to mimic the functionality of componentDidMount()componentDidUpdate(), and componentWillUnmount(). The first argument to useEffect() is a function we want to execute after the component renders, and the second argument is an optional array. This array, known as the dependency array, triggers the useEffect() function input to run only when the component renders and one of the values in the dependency array changes between the current render and the previous render. A function returned from useEffect() acts as componentWillUnmount(), as this function will execute when the component unmounts.

Hooks can only be invoked in React function components, and never in regular JavaScript functions or React class components. They must also only be called at the top-level of the function component, never within conditions, loops, or other functions.

What's Wrong With Classes?

During the React Conf in 2018, the React team highlighted some challenges with React, specifically with class components. They pinpointed three main issues that were prevalent in React until 2018:

Wrapper Hell

It was a common occurrence for websites developed with React to have multiple layers of nesting, resulting in a pyramid of divs encapsulating a deeply nested component.

Large Components/Logic Reuse

Duplication was often seen in class components. For example, in componentDidMount, we might subscribe to some network requests and set recurring events, and then do the exact opposite in componentWillUnmount. This created a sort of negative mirror effect, where these two functions often repeated the same code, but with opposite outcomes.

javascript
componentDidMount(){
    this.subscribeToNetwork(this.props.network);
    this.startTimers();
}
...
componentWillUnmount(){
    this.unsubscribeFromNetwork(this.props.network);
    this.stopTimers();
}

Grasping Classes

The inner mechanics of classes in JavaScript can be quite perplexing, especially when it comes to understanding the need to bind this in our constructor() for methods we define on a class. While this is an intriguing subject, it appears to be a significant mental hurdle to surmount before we can proceed with our primary objective: developing web applications.

Dan Abramov, a member of the React team, was of the opinion that these three issues were all manifestations of a single problem in React: The necessity to use a class component whenever we want to introduce state, irrespective of the component’s size or simplicity. [2]

“React doesn’t offer a stateful primitive simpler than a class component.” - Dan Abramov, React Conf 2018

This is the problem that Hooks intended to solve: they enable state to exist in a functional component.

A Comparison to Class Components

The hooks mechanism allows us to utilize state and lifecycle methods within functional components. There’s no need for a constructor() or this. We simply use straightforward JavaScript functions. For example, how would we handle state in a class component?

javascript
import React from "react";
class App extends React.Component{
    constructor(props){
        super(props);
        this.state = {data: ""};
    }

    render(){
        return <div>Here's the data: {this.state.data}</div>}
};

Initially, we would have to extend from the React.Component class to gain access to the lifecycle methods. Following that, we would need to establish our constructor() and invoke super(), passing in our props to maintain access to those essential lifecycle methods. Lastly, we would have to create a state object using the somewhat troublesome this to store a piece of state that we would want to monitor on the component.

Now, let’s see how we can achieve the same using the hooks system:

javascript
import React, { useState } from "react";
const App = (props) => {
    const [data, setData] = useState("");
    return <div>Here's the data: {data}</div>
};

Dissecting The Concept

What exactly is a Hook? Well, useState() mentioned above is a hook. So are useEffect() and useCallback(). Notice a pattern? Hooks are quite easy to identify as they all follow the naming convention of a use prefix. These hooks enable us to hook into (hence the term) the state and lifecycle of a component. These hooks are predefined within the react library for our use.

import React, { useState } from “react”

The initial step to utilize these hooks is to import them from the “react” library. There are two methods to do this; we can either directly import the desired hook (useState in this instance) from the “react” library using object destructuring syntax, or we can simply invoke useState from the React object that we have already imported. [3] To clarify, the following two methods are identical:

javascript
// Using the React.useState syntax
import React from "react";
React.useState("Initial value");

// Using object destructuring to call useState directly
import React, { useState } from "react";
useState("Initial value")

const [data, setData]

Just as we previously used object destructuring to extract a specific value from an object, we can similarly use array destructuring. However, in this case, it’s mandatory. This method utilizes the index of an array to assign values.

javascript
const myArr = ["Football", "Rugby", "Badminton"];

We previously defined an array named myArr with three values: “Football” at index 0, “Rugby” at index 1, and “Badminton” at index 2. When we apply array destructuring, the variables we define will correspond to the values in myArr based on their index. For example, if we wanted to select “Football” and “Rugby” from the array, we could do so by defining variables at the same index as they appear in the array.

javascript
const myArr = ["Football", "Rugby", "Badminton"];
const [bestSport, worstSport] = myArr;
console.log(bestSport); // "Football"
console.log(worstSport); // "Rugby"

“Football” is at index 0 in myArr, so when we write const [bestSport] = myArr, we’re saying “Assign the first value in myArr to a variable named bestSport”. Similarly, when we write const [sport1, sport2] = myArr, we’re saying “Assign the value at the first index (0) of myArr to a variable named sport1, and the value at the second index of myArr (1) to a variable named sport2”.

This is the principle behind the line const [data, setData]. The useState() function returns an array, and we’re simply taking the first item in the array returned from useState() and assigning it to a variable that we choose to name data, and the second to a variable that we choose to name setData.

The advantage of using array destructuring here is that we get to choose the names for our variables, allowing us to select names for the pieces of state we want (yes, these variables are actually state!). However, by convention, we typically use the naming scheme const [thing, setThing] when defining these variables.

const [data, setData] = useState("")

The real enchantment in the process occurs when we call useState(). The internal mechanics of useState() utilize a JavaScript concept known as closures, which we’ll explore when we delve into scoping in the future. Fortunately, we don’t need to understand how it works to use it effectively (we exchange a complex JS topic like this for an equally complex topic called closures, but in the latter case, we don’t actually need to understand the topic to utilize it).

useState() returns two items: the value we assign to our data variable, and a function we assign to our setData variable. The initial value for our data variable is provided as an argument to useState(). Here’s a mental model to help you understand useState().

javascript
// primitive useState
function useState(initialValue){
    let value = initialValue;

    function setValue(newValue){
        value = newValue;
    }

    return [value, setValue];
}

const [myState, setMyState] = useState("Josh");

// myState is assigned to be "value",
// and setMyState is assigned to be the function "setValue"

For clarity, let’s look at how state is introduced in both a class and a function component.

javascript
// Class component
class MyComponent extends React.Component{
    contructor(){
        super(props);
        this.state = {name: "Josh"};
    }
}

// Functional component using Hooks
const MyComponent = () => {
    const [name, setName] = useState("Josh");
}

But what about the second value we get from useState() that we assign to setData? This serves as a substitute for the this.setState() method, which we can’t access in a functional component. Instead of calling this.setState() to update any piece of state, with hooks, we get a unique function for each piece of state we’re tracking.

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

const App = (props) => {
    const [count, setCount] = useState(0);
    const [name, setName] = useState("Josh");

    console.log(count); // 0 on the first render
    setCount(5); // console.log above now outputs 5 as this causes a re-render

    console.log(name); // "Josh"
    setName("Emily"); // "Emily" logged out above after re-render

    return(
        <div></div>);
};

For each piece of state we wish to monitor, we invoke useState(). This returns two things: the state piece as a variable (countname), and a function specifically for updating that single state piece (setCountsetName).

Invoking setThing() (setCountsetName, etc.) has the same effect as invoking this.setState() in a class component: it triggers a re-render of the component. This also implies that we must adhere to the rule of never directly mutating state: we should never use count = to update the value of count, but should always call setCount(), otherwise our component won’t re-render.

It’s important to note that all aspects of the component lifecycle remain relevant - we’re just using different functions to represent the stages. We still mount a functional hook component, we still update it, and we still unmount it at the end of its lifecycle.

To clarify, in a class component, we define state as this.state = {count: 0} in the constructor() function. To update our state, we call this.setState({count: 1}) to change our count to 1. In a function component using hooks, we set each piece of state separately using the const [count, setCount] = useState(0) syntax, and then update that piece of state using the setCount(1) syntax to set our count to 1.

Common React Hooks

Just as there are numerous lifecycle methods in class components, there are also a variety of hooks available. Fortunately, similar to lifecycle methods, some hooks are used more frequently than others.

useState()

This is arguably the most commonly used hook in contemporary React applications. We’ve already delved into this hook in detail above, so it’s safe to say that this hook is utilized to establish state in our function components (as a result, React now possesses a stateful primitive that’s simpler than a class component!).

useEffect()

While we still mountupdate, and unmount when using hooks, we lose access to the crucial lifecycle methods: componentDidMount(), componentDidUpdate(), and componentWillUnmount() when we use functional components. This is where useEffect() comes into play. useEffect() encapsulates all 3 of the aforementioned lifecycle methods into a single function.

javascript
import React, { useState, useEffect } from "react";
const App = (props) => {
    const [count, setCount] = useState(0);
    
    useEffect( () => {
        // arrow function as input for useEffect
    });

    return(
        <div>
            The count is {count}
        </div>);
};

The useEffect() hook primarily consists of an arrow function that we provide as its first argument. This function is designed to execute each time the component renders, similar to componentDidUpdate(), but it also runs during the initial render.

An optional second argument can be passed to useEffect(), known as the dependency array, which we’ll delve into later. This is why useEffect() is somewhat a combination of componentDidMount() (which executes on the first render) and componentDidUpdate() (which executes on every subsequent render).

Let’s consider a Counter example for better understanding:

javascript
import React, { useState, useEffect } from 'react';

const Counter = () => {
    const [count, setCount] = useState(0);

    useEffect(() => {
        document.title = `Count = ${count}`;
    });

    return (
        <div>
            <p>The current count is {count}</p>
            <button onClick={() => setCount(count + 1)}>
               Increment
            </button>
        </div>);
}

In this case, we establish count as a state, and then construct a <button /> that will increase the count when clicked (onClick={() => setCount(count+1)}). During the initial render, the function within our useEffect() hook is executed and it updates the title of our document to reflect the current count (0).

Upon clicking the button to increase our count, our component undergoes a re-render and the function within our useEffect() is executed once more, refreshing our title to reflect the updated value of count.

I previously referred to the dependency array, so let’s discuss that further. The dependency array is an optional second argument that can be passed to useEffect().

useEffect(someFunction, []);: This array provides us with finer control over when the function we supply to useEffect() is executed. Maybe we don’t want the useEffect() to run each time the component re-renders; perhaps there’s a particular useEffect() that we only want to trigger when a specific state changes. Let’s consider the counter example from earlier:

javascript
import React, { useState, useEffect } from 'react';
const Counter = () => {
    const [count, setCount] = useState(0);
    const [onlineStatus, setOnlineStatus] = useState(false);

    useEffect(() => {
        document.title = `Count = ${count}`;
    });

    return (
        <div>
            <p>The current count is {count}</p>
            <p>{onlineStatus === true ? "You're online!" : "You're NOT online!"}</p>
            <button onClick={() => setCount(count + 1)}>
               Increment
            </button>

            <button onClick={() => setOnlineStatus(!onlineStatus)}>
               Toggle online status
            </button>
        </div>);
}

We have two buttons in this scenario. One button updates the count value, while the other toggles the onlineStatus (a dummy state variable used for demonstration). When a user uses the button to change their online status, the component re-renders to reflect the updated onlineStatus value. This action triggers the useEffect() function, which updates our document title to match the current count value. However, if the count value hasn’t changed since the last render, updating the document.title is unnecessary and executing the useEffect() function would be an inefficient use of resources.

To avoid this inefficiency, we can specify that the function within this specific useEffect() should only execute when the count value changes.

javascript
import React, { useState, useEffect } from 'react';
const Counter = () => {
    const [count, setCount] = useState(0);
    const [onlineStatus, setOnlineStatus] = useState(false);

    useEffect(() => {
        document.title = `Count = ${count}`;
    }, [count]); // dependency array here

    return (
        <div>
            <p>The current count is {count}</p>
            <p>{onlineStatus === true ? "You're online!" : "You're NOT online!"}</p>
            <button onClick={() => setCount(count + 1)}>
               Increment
            </button>

            <button onClick={() => setOnlineStatus(!onlineStatus)}>
               Toggle online status
            </button>
        </div>);
}

Following each render, React examines the dependency array for each useEffect(). It compares the variables’ previous values (before re-rendering) in the array with their current values (after re-rendering). If any of the values have altered, React will execute that useEffect(). If the most recent render did not modify any value in the dependency array, React will opt not to execute that useEffect().

Hence, we could have multiple state pieces in our dependency array, for example [a, b, c], and the function passed into that useEffect() would only execute if a and/or b and/or c change. We can also include props in our dependency array, in addition to state pieces (remember, our component re-renders upon new state and/or new props). Lastly, we can pass an empty array to the dependency array.

javascript
import React, { useState, useEffect } from 'react';
const Counter = () => {
    const [count, setCount] = useState(0);

    useEffect(() => {
        console.log("Only on the initial render")
    }, []);

    return (
        <div>
            <p>The current count is {count}</p>
            <button onClick={() => setCount(count + 1)}>
               Increment
            </button>
        </div>);
}

By passing an empty array, we instruct React to execute this useEffect() only on the initial render, similar to how componentDidMount() is used. Essentially, we’re saying “execute this only if any of the values in the dependency array change. However, since the dependency array is empty, none of its values can change, so it will only run at startup.”

It’s important to note that we can have multiple useEffect() calls within a single component. For instance, if we want the document.title to reflect any recently changed state (like the counter from the previous example), we could add a second useEffect() that only executes when onlineStatus changes.

javascript
import React, { useState, useEffect } from 'react';
const Counter = () => {
    const [count, setCount] = useState(0);
    const [onlineStatus, setOnlineStatus] = useState(false);

    useEffect(() => {
        document.title = "Count changed!";
    }, [count]);

        useEffect(() => {
        document.title = "onlineStatus changed!";
    }, [onlineStatus]);

    return (
        <div>
            <p>The current count is {count}</p>
            <p>{onlineStatus === true ? "You're online!" : "You're NOT online!"}</p>
            <button onClick={() => setCount(count + 1)}>
               Increment
            </button>

            <button onClick={() => setOnlineStatus(!onlineStatus)}>
               Toggle online status
            </button>
        </div>);
}

In this scenario, we employ two useEffect() calls with distinct dependency arrays, enabling us to update the document.title to mirror the most recent state alteration.

Having addressed componentDidMount() and componentDidUpdate(), how do we utilize useEffect() to emulate componentWillUnmount()? Observe that we haven’t returned anything from our useEffect() functions yet! They are merely functions that execute when the component re-renders (based on their dependency array) and lack a return statement. This is because the return statement serves as componentWillUnmount(). In other words, when we return a function from a useEffect(), the function will execute when the component unmounts.

For instance, in a class-based component, if we establish an interval in componentDidMount(), we would need to clean it up again with componentWillUnmount().

javascript
import React from "react";
class App extends React.Component{

    componentDidMount(){ 
        this.interval = setInterval(() => alert("Hey I'm still here!"), 5000);
    }

    componentWillUnmount(){
        clearInterval(this.interval);
    }

    render(){
        return (
            <div>
                Hello, world!
            </div>);
    }
};

We could accomplish the same result by utilizing the useEffect() hook in a functional component!

javascript
import React, { useEffect } from "react";
const App = () => {
    useEffect( () => {
        let interval = setInterval(() => alert("Hey I'm still here!"), 5000);

        return ( () => {
            clearInterval(interval);
        });

    }, []);

    return (
        <div>
            Hello, world!
        </div>);
};

In useEffect(), we establish an interval that only initiates on the first render due to an empty dependency array. We then proceed to clean it up by invoking clearInterval() within a function returned from useEffect(). This is equivalent to invoking componentWillUnmount() to clear the interval.

The useEffect(function, []) essentially takes the place of componentDidMount(), componentDidUpdate(), and componentWillUnmount().

The hooks useState() and useEffect() are predominantly used. While other hooks exist and have their specific uses, they are more specialized and won’t be discussed here. Essentially, hooks can be utilized as a direct substitute for class components and their associated lifecycle methods.

Rules of React Hooks

Hooks are indeed beneficial, but they come with a couple of stipulations:

They Should Only Be Used At The Top-Level of A Component

The term “top-level” here pertains to the primary body of your functional component. This implies that Hooks should not be invoked within conditions, other functions, or loops. We can’t stipulate “use this hook only if a certain condition is met”, or “invoke this hook within this loop”.

javascript
import React, { useState, useEffect } from "react";
const App = () => {

    if(/* some condition */){
        const [thing, setThing] = useState("");
        // NOT ALLOWED!
    };

    return (
        <div>
            Hello, world!
        </div>);

};

Trying to invoke a hook outside of the main function body will result in an error. For better understanding, I have explicitly demonstrated where the top-level is in a given functional component, and where it isn’t.

javascript
import React, { useEffect } from "react";
const App = () => {
    // Top level
    // Top level
    if(/* some condition */){
        // NOT top level
    };
    // Top level
    for (let i = 0; i < 3; i++){
        // NOT top level
    }
    // Top level
    const someFunc = () => {
        // NOT top level
    };
    // Top level
    return (
        //NOT top level
        <div>
            Hello, world!
        </div>);
};

A useful guideline is that if there are curly braces/parentheses enclosing it that aren’t the primary curly braces/parentheses for your function component, it’s likely not at the top level.

Therefore, it’s frequently recommended to define all hooks at the beginning of the component, prior to declaring any auxiliary functions or other variables.

javascript
import React, { useState, useEffect } from "react";
const App = () => {
    // all useState calls

    // all useEffect calls

    // Custom variables/functions

    return (
        <div>
            Hello, world!
        </div>);

};

Invoke Them Solely from React Function Components

Hooks are exclusively operational within a React function component. Invoking them within a class component or standard JS functions will not yield any results.

We’ll revisit this topic another day - while invoking hooks in standard JS functions is not allowed, we CAN invoke them from Custom Hooks, which are essentially JS functions that utilize the Hook system.

Why?

The rationale behind this is rooted in the fundamental workings of the Hook system. It necessitates that all hooks are presented in the exact same order during every render.

javascript
const [one, setOne] = useState("one"); // Always first, every render
const [second, setSecond] = useState("second"); // Always second, every render
const [third, setThird] = useState("Third"); // Always third, every render

If we establish our hooks in a specific sequence during the initial render, React anticipates this sequence to remain consistent in every render. However, if we opt to invoke a hook conditionally:

javascript
const [one, setOne] = useState("one"); // First
if(/* condition */){
    const [second, setSecond] = useState("second"); // Sometimes second.. sometimes not called
};

const [third, setThird] = useState("Third"); // Sometimes second, sometimes third

This could significantly disrupt the sequence of our hooks. It’s important to note that this also applies to useEffect(). The following scenario is not permissible.

javascript
const [one, setOne] = useState("one"); // First hook 
if(/* condition */){
    useEffect(() => conosle.log("something")) // Sometimes second hook.. sometimes not called
};

const [third, setThird] = useState("Third"); // Sometimes second hook, sometimes third hook

Keep in mind: Even if the function we provide to useEffect isn’t triggered because of its dependency array, useEffect() is technically still invoked, which is acceptable for this context. Dependency arrays won’t create problems related to the order of hook calls.

Hence, if we need to incorporate conditions or loops, we should place them within our hook. For instance:

javascript
if(name === "josh"){
    useEffect(() => console.log("Hi josh"));
    // NOT OKAY
};

useEffect(() => {
    if (name === "josh") {
        console.log("Hi josh");
        // Okay!
    };
});

// So long as the useEffect is called
// in the top level, we can put
// any conditions or loops inside the the useEffect

Counter

As we conclude this section on hooks, let’s revisit our Counter component from an earlier article.

javascript
import React from "react";
import ReactDOM from "react-dom";

 class Counter extends React.Component {
    constructor(props){
        super(props);
        this.state = {count : 0}
    };

    increment(){
        this.setState({count: this.state.count + 1});
    };

    decrement(){
        this.setState({count: this.state.count - 1});
    };

    render(){
        return(
            <div>
                <h1>The count is: {this.state.count}</h1>
                <button onClick={() => this.increment()}>Increment</button>
                <button onClick={() => this.decrement()}>Decrement</button>
            </div>);
    };
};

ReactDOM.render(
    <Counter />,
    document.querySelector("#root")
);

We have the option to refactor (restructure code to enhance readability or adhere to different design principles) this component to utilize hooks.

Initially, we transform our class into a function and import useState.

javascript
import React, { useState } from "react";
// import useState
import ReactDOM from "react-dom";

 const Counter = () => { // create a function instead of a class
    constructor(props){
        super(props);
        this.state = {count : 0}
    };

    increment(){
        this.setState({count: this.state.count + 1});
    };

    decrement(){
        this.setState({count: this.state.count - 1});
    };

    render(){
        return(
            <div>
                <h1>The count is: {this.state.count}</h1>
                <button onClick={() => this.increment()}>Increment</button>
                <button onClick={() => this.decrement()}>Decrement</button>
            </div>);
    };
};

ReactDOM.render(
    <Counter />,
    document.querySelector("#root")
);

Subsequently, we’ll use the useState() hook to create our state, rather than employing a constructor().

javascript
import React, { useState } from "react";
import ReactDOM from "react-dom";

 const Counter = () => {
    const [count, setCount] = useState(0); // useState instead of a constructor

    increment(){
        this.setState({count: this.state.count + 1});
    };

    decrement(){
        this.setState({count: this.state.count - 1});
    };

    render(){
        return(
            <div>
                <h1>The count is: {this.state.count}</h1>
                <button onClick={() => this.increment()}>Increment</button>
                <button onClick={() => this.decrement()}>Decrement</button>
            </div>);
    };
};

ReactDOM.render(
    <Counter />,
    document.querySelector("#root")
);

Following that, we can eliminate the increment and decrement methods, as we have the ability to update our count using setCount().

javascript
import React, { useState } from "react";
import ReactDOM from "react-dom";

 const Counter = () => {
    const [count, setCount] = useState(0);
    // removed increment/decrement methods
    render(){
        return(
            <div>
                <h1>The count is: {this.state.count}</h1>
                {/* setCount to increment/decrement */}
                <button onClick={() => setCount(count + 1))}>Increment</button>
                <button onClick={() => setCount(count - 1)}>Decrement</button>
            </div>);
    };
};

ReactDOM.render(
    <Counter />,
    document.querySelector("#root")
);

We can then eliminate the unnecessary render method.

javascript
import React, { useState } from "react";
import ReactDOM from "react-dom";

 const Counter = () => {
    const [count, setCount] = useState(0);
    // render() removed
    return(
        <div>
            <h1>The count is: {this.state.count}</h1>
            <button onClick={() => this.increment()}>Increment</button>
            <button onClick={() => this.decrement()}>Decrement</button>
        </div>);
};

ReactDOM.render(
    <Counter />,
    document.querySelector("#root")
);

Ultimately, we can simplify this.state.count to just count

javascript
import React, { useState } from "react";
import ReactDOM from "react-dom";

 const Counter = () => {
    const [count, setCount] = useState(0);

    return(
        <div>
            <h1>The count is: {count}</h1>
            <button onClick={() => setCount(count + 1)}>Increment</button>
            <button onClick={() => setCount(count - 1)}>Decrement</button>
        </div>);
};

ReactDOM.render(
    <Counter />,
    document.querySelector("#root")
);

And that’s it! This is identical to the Counter component we previously created using classes. You can directly copy and paste this into the index.js of your CRA project folder to get a neat Counter app, but this time using hooks! Personally, I find this much cleaner and easier to comprehend than dealing with a constructor()this, and a state object. What do you think?

useReducer(): Another Approach Besides useState()

When you employ the useState() hook to handle a complex state such as an item list, where adding, updating, and removing items is necessary, you’ll observe that a significant portion of the component’s body is consumed by state management logic.

Typically, a React component should encapsulate the logic that determines the output. However, state management logic is a separate concern that should be handled independently. Otherwise, you end up with a blend of state management and rendering logic in one location, which can be challenging to read, maintain, and test!

React offers the useReducer() hook to assist in separating these concerns (rendering and state management). This hook achieves this by moving the state management outside of the component.

How Does the useReducer() Hook Operate?

The useReducer(reducerFunction, initState) hook takes in 2 parameters: the reducerFunction and the initState. The hook then gives back an array of 2 elements: the currentState and the dispatchFunction.

javascript
import { useReducer } from 'react';

function MyComponent() {
  const [currentState, dispatchFunction] = useReducer(reducerFunction, initState);

  const actionObject = {
    type: 'ActionType'
  };

  return (
    <button onClick={() => dispatchFunction(actionObject)}>
      Click me
    </button>
  );
}

Now, let’s unravel the meaning of initState, actionObject, dispatchFunction, and reducerFunction.

Starting Point:

The starting point is the initial value that the state begins with.

For instance, in the scenario of a tally state, the starting value would be:

javascript
// starting point
const startingPoint = { 
  tally: 0 
};

Command Object:

A command object is an object that outlines how to modify the state.

Typically, the command object has a property named ‘kind’ — a string that describes the type of state modification the reducer should perform.

For instance, a command object to increment the tally could look like this:

javascript
const command = {
  kind: 'increment'
};

If the command object needs to carry some valuable data (also known as payload) to be utilized by the reducer, then you can add extra properties to the command object.

For instance, here’s a command object intended to append a new member to the members state array:

javascript
const command = {
  kind: 'append',
  member: { 
    name: 'Jane Doe',
    email: 'jdoe@mail.com'
  }
};

‘member’ is a property that holds the information about the member to append.

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

Dispatch Method:

The dispatch is a unique method that dispatches a command object.

The dispatch method is created for you by the useReducer() hook:

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

Whenever you wish to modify the state (usually from an event handler or after completing a fetch request), you simply call the dispatch method with the appropriate command object: dispatch(commandObject).

In simpler terms, dispatching means a request to modify the state.

Reducer Function

The reducer is a pure function that accepts 2 parameters: the current state and a command object. Depending on the command object, the reducer function must modify the state in an immutable manner, and return the new state.

The following reducer function supports the increment and decrement of a tally state:

javascript
function reducer(state, command) {
  let newState;
  switch (command.kind) {
    case 'increment':
      newState = { tally: state.tally + 1 };
      break;
    case 'decrement':
      newState = { tally: state.tally - 1 };
      break;
    default:
      throw new Error();
  }
  return newState;
}

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

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.

Connecting the Dots:

When an event handler triggers or a fetch request completes, it calls the dispatch function with an action object.

React then takes this action object and the current state value and passes them to the reducer function.

The reducer function takes these inputs, performs a state update, and returns the new state.

React checks if this new state is different from the previous one. If there’s a change, React re-renders the component. The useReducer() function then returns the new state value: [newState, …] = useReducer(…).

It’s important to note that the design of useReducer() is based on the Flux architecture.

If these terms seem too theoretical, don’t worry! We’ll explore how useReducer() works with a practical example next.

Example

We’re going to create a stopwatch with three buttons: ‘Begin’, ‘Halt’, ‘Clear’, and a display showing the elapsed seconds.

Let’s first consider how to structure the state of the stopwatch.

There are two crucial state properties: a boolean indicating if the stopwatch is active isActive, and a number showing the elapsed seconds  elapsedTime. Here’s what the initial state might look like:

javascript
const initialState = {
  isActive: false,
  elapsedTime: 0
};

The initial state shows that the stopwatch starts as inactive and at 0 seconds.

Next, let’s think about what action objects the stopwatch should have. We need four types of actions: to begin, halt and clear the stopwatch process, as well as increment the time each second.

javascript
// The begin action object
{ type: 'begin' }

// The halt action object
{ type: 'halt' }

// The clear action object
{ type: 'clear' }

// The increment action object
{ type: 'increment' }

With the state structure and the possible actions in mind, let’s use the reducer function to define how the action objects update the state:

javascript
function reducer(state, action) {
  switch (action.type) {
    case 'begin':
      return { ...state, isActive: true };
    case 'halt':
      return { ...state, isActive: false };
    case 'clear':
      return { isActive: false, elapsedTime: 0 };
    case 'increment':
      return { ...state, elapsedTime: state.elapsedTime + 1 };
    default:
      throw new Error();
  }
}

Finally, here’s the Stopwatch component that brings everything together by invoking the useReducer() hook:

javascript
import { useReducer, useEffect, useRef } from 'react';

function Stopwatch() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const idRef = useRef(0);

  useEffect(() => {
    if (!state.isActive) { 
      return; 
    }
    idRef.current = setInterval(() => dispatch({ type: 'increment' }), 1000);
    return () => {
      clearInterval(idRef.current);
      idRef.current = 0;
    };
  }, [state.isActive]);
  
  return (
    <div>
      {state.elapsedTime}s
      <button onClick={() => dispatch({ type: 'begin' })}>
        Begin
      </button>
      <button onClick={() => dispatch({ type: 'halt' })}>
        Halt
      </button>
      <button onClick={() => dispatch({ type: 'clear' })}>
        Clear
      </button>
    </div>
  );
}

The click event handlers of the ‘Begin’, ‘Halt’, and ‘Clear’ buttons use the dispatch() function to dispatch the corresponding action object.

Inside the useEffect() callback, if state.isActive is true, the setInterval() timer function dispatches the ‘increment’ action object each second dispatch({type: 'increment'}).

Each time the reducer() function updates the state, the component receives the new state and re-renders.

Summary

To further reinforce your understanding, let’s consider a real-world analogy that operates similarly to a reducer.

Picture yourself as the skipper of a vessel in the early 1900s.

The ship’s bridge is equipped with a unique communication apparatus known as the engine order telegraph (refer to the image above). This device is utilized to relay orders from the bridge to the engine room. Common orders might include slow reverse, half speed ahead, full stop, and so on.

You’re on the bridge and the ship is stationary. As the skipper, you want the ship to proceed at maximum speed. You would approach the engine order telegraph and position the handle to full ahead. The engineers in the engine room, possessing an identical device, observe the full ahead order and adjust the engine to the corresponding setting.

The engine order telegraph represents the dispatch function, the orders symbolize the action objects, the engineers in the engine room act as the reducer function, and the engine setting is the state.

The engine order telegraph aids in segregating the bridge from the engine room. Similarly, the useReducer() hook assists in separating the rendering from the state management logic.

To Conclude

The useReducer() hook allows you to decouple the state management from the component’s rendering logic.

const [state, dispatch] = useReducer(reducer, initialState) takes in two parameters: the reducer function and the initial state. The reducer then returns a pair: the current state and the dispatch function.

To modify the state, invoke dispatch(action) with the suitable action object. This action object is then passed to the reducer() function that alters the state. If the state is updated by the reducer, the component re-renders, and [state, ...] = useReducer(...) hook yields the new state value.

useReducer() is ideal for handling complex state management that requires at least 2-3 update actions. For simpler state management, useState() is recommended.

How frequently do you utilize useReducer() in comparison to useState()?

>> Read more: 

Conclusion

Hope this article gives you an overview of React Hooks and lets you build on the aforementioned examples to make wonderful things. Please spread this article if it helped.

>>> Follow and Contact Relia Software for more information!

  • coding
  • development