A simple (typescript) guide to React Hooks - part 2

A simple (typescript) guide to React Hooks - part 2

This article is part of a series. You can find part 1 here.

In the first article we focused on useState and we saw some details about implementing it using typescript. This time we'll focus on useEffect which probably is the second most used hook.
Often, in order to understand useEffect, guides and documentation makes lot of reference to component lifecycle. This is totally correct and may help you to better understand when the effect callback is called and why. The problem raises when you're not familiar with the component lifecycle, when you never really got what componentDidMount or componentDidUpdate do and when. Just to be clear, it would be better to really understand those concepts to fully understand how a React component works. On the other hand I'll try to explain useEffect without doing any parallelism with these old class methods. From time to time I'll talk about "when the component mount", "before or after render", but still I'll try to talk about useEffect as classes never existed and we always had just hooks.

What is an effect?

We're going to start, using the same code we produced at the end of the previous article, let me re-post it here

import React, { useState } from 'react';

interface HookTestProps {
  startAt?: number
}

const Counter: React.FC<HookTestProps> = ({startAt = 0}) => {
  const [count, setCount] = useState<number>(startAt);
  
  return <div>
    <p>{count}</p>
    <button onClick={() => setCount(count => count + 1)}>Add 1</button>
    <button onClick={() => setCount(count => count - 1)}>Rem 1</button>
  </div>
}

export default Counter;

We have two buttons that can increase/decrease a counter, simple.

Our next goal is to automatically increase the number every X seconds, regardless of the user interaction. For this task we need to use the useEffect hook, which, as the name says, let us handle (side) effects. An effect is everything that happens outside of the normal flow of your components: you need to perform a fetch request to get some data to populate your component? Effect. You need to apply some animation? Effect. In our case we want to increase the number without the user interaction, just every X seconds. Let's write the first version of the effect.

interface HookTestProps {
  startAt?: number
  autoIncreaseTime?: number
}

const Counter: React.FC<HookTestProps> = ({startAt = 0, autoIncreaseTime}) => {
  const [count, setCount] = useState<number>(startAt);
  
  useEffect(() => {
    if(autoIncreaseTime && autoIncreaseTime > 0) {
      setInterval(() => {
        setCount(c => c + 1);
      }, autoIncreaseTime);
    }
  });
  
  return <div>
    <p>{count}</p>
    <button onClick={() => setCount(count => count + 1)}>Add 1</button>
    <button onClick={() => setCount(count => count - 1)}>Rem 1</button>
  </div>
}

Lets see what's happening: the useEffect hook accepts a function and runs it. Our function set an interval every autoIncreaseTime milliseconds and increases the counter by 1. There are rules to understand when the effect will run your function, in this case your function is called every time your component is rendered. Oops... we have a couple of problems. If the function is called every time the component is rendered, we don't know how many intervals will run! If the component disappear from our application (is not shown anymore for any reason) the interval will still run because we never wrote a line of code to disable it.

We need a way to control when our function runs and a way to stop its execution.

To control when our effects run, we need to specify the second argument of the useEffect hook, which is an array describing the dependencies of our effect. React will look at those dependencies and, when they'll change, the effect will be run again. In our case we want to setup a new setInterval any time we have a new autoIncreaseTime. If that value changes our old interval is not valid anymore and want to create a new one.

  useEffect(() => {
    if(autoIncreaseTime && autoIncreaseTime > 0) {
      setInterval(() => {
        setCount(c => c + 1);
      }, autoIncreaseTime);
    }
  }, [autoIncreaseTime]);
Set an array of dependencies for our effect

There are use cases for passing an empty array or not passing that array at all, we'll talk about them in a future article.
The first problem is solved, we now explicitly  tell React when to run our effect.

Let's solve the second problem: whenever the autoIncreaseTime changes, or our component gets removed, we want to stop the interval. To do this we need to return a cleaning effect function: basically we can return a function that clean our effect and React will call it when needed. Let's transform our code to clean the effect:

useEffect(() => {
    if(autoIncreaseTime && autoIncreaseTime > 0) {
      const interval = setInterval(() => {
        setCount(c => c + 1);
      }, autoIncreaseTime);
      
      // Instruct react on how to clean our effect
      return () => {
        clearInterval(interval)
      }
    }
  }, [autoIncreaseTime]);
Clean the effect

All of our problems are solved and we described the basic usage of the effect hook. The useEffect hook can be one of the most complicated because dependency definition, run condition and clean process are left to you. This is why some lint rules exists and may help you, especially to fulfill the dependency array automatically. The only thing to remember is that these lint rules work only for some cases (the most common) but may not work as intended in some situations, so it's better to understand on our own what to do.

Next time we'll see how to build a custom hook and some traps hidden in the hooks lifecycle. See you soon, meanwhile you can find the code of this article on codesandbox

Edit Hooks guide 2