Boldizsár's programming blog

A beginner's guide to React Hooks

June 16, 2019 | 14 Minute Read

React Hooks have been out for a while now and they're awesome. Let's look at the basics.

What is a React Hook?

The idea behind Hooks is that you can use state and React features in a function component so there’s no need to create a class.

Why were Hooks born first of all?

If we take a look at why we are using HoCs (Higher Order Components) the most then we can see it’s because we don’t want to repeat ourselves. When we have the same logic in more than one component we usually use a HoC component (a component which takes a component and returns a component) to outsource the common logic and avoid code duplication. It’s always good because it’s easier to modify and our code can be tested easier too. With React Hooks we don’t need to create a HoCs but we can use a custom Hook which can be used in many components. What is more if we open the React dev tools and look at the component hierarchy we can see that having HoC means that we’ll have a deeply nested hierarchy. With Hooks we can get ride of some levels.

Looking at the lifecycle methods like componentDidMount or componentWillUnmount we see that they can get quite complex. A component might subscribe in componentDidMount to a couple of listeners that can be totally unrelated to each other. But of course, to avoid memory leak we should never forget about their unsubscription in componentWillUnmount. As you can image unrelated data subscriptions and unsubscriptions will get into the same function which breaks the law of separation of concerns. It’d be nice if a function could handle the attachment and detachment of a listener. Well, React Hooks solves this too.

When we create a component with React Hooks it won’t be a class anymore, we don’t have to worry about the this keyword. We won’t forget to bind the handlers to this and our code won’t get more complex because of lines of bindings.

Let’s see some code

Alright, I guess the best way to introduce Hooks is to show a without and a with Hooks implementation of the same component and logic. There are built in Hooks which we’re going to look at. It’s important to note that all built in Hooks’ name starts with use and it’s not by accident. The React Dev team highly recommends starting the name of your custom Hooks by use too.

useState Hook

Let’s create an example where we have a button which toggles a text between “On” and “Off” and we print this text on the screen. This is one of the implementations using a class.

import React from 'react';

class ToggleExample extends React.Component {
  constructor() {
    super(null);
    this.state = {
      isOn: false
    };
  }

  render() {
    const { isOn } = this.state;
    return (
      <>
        <h2>{isOn ? "On" : "Off"}</h2>
        <button onClick={() => this.setState({ isOn: !isOn })}>Toggle</button>
      </>
    )
  }
}

Let’s see how the same component looks using the useState Hook.

import React, { useState } from 'react';

const ToggleExample = () => {
  const [isOn, setIsOn] = useState(false);

  return (
    <>
      <h2>{isOn ? "On" : "Off"}</h2>
      <button onClick={() => setIsOn(!isOn)}>Toggle</button>
    </>
  )
};

Okay, so what is this useState? It is actually our first Hook. As you can see in order to use we have to import it. This Hook enables us to add state to our functional components. Before functional components were only presentational components with no state. With React Hooks this has changed.

Calling useState basically creates a state variable for us. useState actually returns a pair of values. Using array destruction, we can assign these returned values to variables and we can name them as we wish. The first element is the variable which holds the state and the second is a function reference which we can use to update the state. So basically isOn is like this.state.isOn and setIsOn is like this.setState. Even though this is a functional component the value of isOn will be preserved. You can ask what the false value is I passed to useState. The useState function takes a single parameter which will be the initial state of the isOn state variable.

You can access the state variable by its name and as you can see there’s no need to use this.state for that. The update happens by using the setIsOn function. You can pass the new value or you can pass a function whose first parameter will be the old value and you return the new value. So this way you can access the old isOn value if you need that.

It is possible of course to create multiple state variables. Just call useState for each state variable. Note that you can store anything It can be an array, object too. Like in this example userData is an object, it might contain the user’s name, age and other data.

const Example = () => {
  const [isOn, setIsOn] = useState(false);
  const [userData, setUserData] = useState({});
};

It’s important to understand that unlike calling this.setState calling setUserData will NOT merge the state but will replace it. This is a big difference and you should pay attention to it.

useEffect Hook

The second most important/used Hook is the useEffect Hook. Its name suggests it has to do with effects, to be precise with side effects. What our side effects? The term “side effect” is not a React specific term, it is an expression used in computer science. It means that a function or expression changes some state that is outside of its scope. So basically, if you execute that multiple times it might give you a different result and might do different things. For example pushing a new element to the array received via params or logging or making a network request. In all these cases there might be certain scenarios when you will see different results.

In React side effects are like mutation, calling an API, logging, subscribing to a listener. In class components we executed these effect in functions like componentDidMount, componentDidUpdate and componentWillUnmount. We should never ever call any code which has a side effect in the render phase since it can lead to bugs and inconsistent UI.

The useEffect Hook combines the above three methods into one. useEffect will have two parameters. The first one is a function which React will call after the DOM has finished rendering. (Yes, after every render, even when the component has just mounted). We’ll take a look at the second parameter a bit later. Let’s see an example now where we’ll update our previous example by printing the isOn state variable’s value in the document’s title after every update.

import React from 'react';

class ToggleExample extends React.Component {
  constructor() {
    super(null);
    this.state = {
      isOn: false
    };
  }
  
  componentDidMount() {
    document.title = this.state.isOn;
  }
    
  componentDidUpdate() {
    document.title = this.state.isOn;
  }

  render() {
    const { isOn } = this.state;
    return (
      <>
        <h2>{isOn ? "On" : "Off"}</h2>
        <button onClick={() => this.setState({ isOn: !isOn })}>Toggle</button>
      </>
    )
  }
}

Now let’s see how it’s done with the useEffect Hook.

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

const ToggleExample = () => {
  const [isOn, setIsOn] = useState(false);
  
  useEffect(() => {
    document.title = isOn;
  });

  return (
    <>
      <h2>{isOn ? "On" : "Off"}</h2>
      <button onClick={() => setIsOn(!isOn)}>Toggle</button>
    </>
  )
};

As you can see, we pass a function to the useEffect Hook which React runs after every update to the DOM. You can also see that componentDidMount and componentDidUpdate was merged by that Hook. Since the function is inside our functional component, we can access our state variable created by useState.

You can notice that on each render a function will be passed to the useEffect Hook. It means two things. One is that our effect function will always have the latest prop and state values. The other is that even if the props or state values used in the passed function hasn’t changed the effect will still run again meaning that it executes the same thing which might not be necessary and can lead to performance issues. What’s the solution?

React API provides a way to be able to tell React when we want the effect to run. This is when the second argument comes into the picture. We can pass an array to useEffect and list those values which we care about in our effect. What React will do is that it compares each element of the array to its previous value and if at least one element doesn’t have the same value as it had in the previous render then it will execute the effect. Let’s update our component so that it only updates the title when there’s been a change to the isOn state variable.

const ToggleExample = () => {
  const [isOn, setIsOn] = useState(false);
  
  useEffect(() => {
    document.title = isOn;
  }, [isOn]);

  return (
    <>
      <h2>{isOn ? "On" : "Off"}</h2>
      <button onClick={() => setIsOn(!isOn)}>Toggle</button>
    </>
  )
};

As you can see I added an array as the second parameter of useEffect. React compares the current and the previous isOn value and will only run the effect function if the two values are different. So this is how we can control if we want to run the effect or not.

Bugs can occur if you don’t list a value which is used in the effect. Let’s say there was a prop which is not listed in the array next to isOn but it’s used the effect function. In this case since React won’t compare it to its previous value the effect won’t run again which can be an undesired flow so pay attention to that.

We’ve not talked about componentWillUnmount and how it is used with the useEffect Hook. Let’s say we have some data source to which we subscribe in componentDidMount and we would unsubscribe in componentWillUnmount. How can we achive the same thing using the effect Hook. We of course don’t want to subscribe on each render (componentDidUpdate). How can we prevent our effect Hook from running after every render? Well, we have to use the second parameter. If we pass an empty array ([]) React compares each of its elements to the previous elements and since it’s an empty array it won’t see any difference so it won’t run the useEffect Hook. So we solved one of the problems. Now the effect only runs when the component got mounted and not on “regular” updates.

Our effect function which we pass to the useEffect Hook can actually return a function which is its clean-up procedure. Since we separate our logic by effects and not by lifecycle methods, React creators designed the API so that each effect should be responsible for its clean-up. With class components if we subscribed, started a counter, etc. we had to clean these up in the componentWillUnmount method. Using the useEffect Hook we have to return a function that React will call before it would register a new effect. Let’s see an example.

const SubscriptionExample = () => {
  const [message, setMessage] = useState(null);

  useEffect(() => {
    Socket.subscribe('event', (msg) => {
      setMessage(msg);
    });
    return () => {
      Socket.unsubscribe('event');
    }
  }, []);

  return <h2>{message}</h2>;
};

As you can see, we return a clean-up function from our effect which should be responsible for removing all subscriptions, listeners, etc. Also I pass an empty array as the second parameter to prevent subscribing and unsubscribing on each render as it might cause issues.

You might have noticed that a new function is created on each render regardless of the second parameter. Using the second parameter of useEffect we can only prevent the effect from running but a new effect function is always created. Nowadays that shouldn’t cause any performance issues.

Summary

This was a guide to the two most used built in React Hooks. If you wish to read more on other built in Hooks or how you can create your own Hook visit the official site. I’m planning to write an article about those too and will link them if ready.

Until the next one, take it easy.