Course: Section

Get Started with Modern React: Step by Step

Episode: Title

S02・V09: State (Part 2)

Date Created: July 17th, 2019
Last Updated: 27 days ago

Objectives
  1. We will show you how to encapsulate a component that updates its state.
  2. We will show you how to control when the “effect” function is run.
  3. We will show you how to clean up side-effects when a component is removed from the DOM.
Watch Video
Duration: 9m 29s

Ticking Clock

Consider the ticking clock example from a previous video entitled “Rendering Elements”. In this video, we will show you how to make the Clock component truly reusable and encapsulated. It will set up its own timer and update itself every second.

To start with, let’s reimplement the ticking clock as we had it before. We will import react and react-dom libraries, add the Clock component, and render the component to the DOM, passing in a new Date() as a date prop.

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

function Clock(props) {
  return (
    <div>
      <h1>Hello, World!</h1>
      <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}

ReactDOM.render(
  <Clock date={new Date()} />,
  document.getElementById("root")
);

We can now see the Clock component on our page in Chrome, with a static time stamp.

To have the time update, we wrapped the DOM render in a function, and used an interval timer to call the function every second.

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

function Clock(props) {
  return (
    <div>
      <h1>Hello, World!</h1>
      <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}

function tick() {
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById("root")
  );
}

setInterval(tick, 1000);

Now the clock updates every second.

Reusability

However, the Clock component is not reusable since its most important feature, which is that it ticks every second, is implemented outside of the component.

It would be better if the Clock component set up the interval timer within itself, so that it is more encapsulated and reusable.

The ReactDOM.render() should look like this:

ReactDOM.render(<Clock />, document.getElementById("root"));

Let’s remove the tick function and setInterval() timer. We need to add “state” to the Clock component. We will use React’s state hook. The state that the Clock component will keep track of is the current date. First, we will define the initial date. Now, we will use the useState hook, to obtain the date getter and setter. Next, we can remove the references to props, since we are using encapsulated “state” instead.

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

function Clock() {  const initialDate = new Date();  const [date, setDate] = useState(initialDate);
  return (
    <div>
      <h1>Hello, World!</h1>
      <h2>It is {date.toLocaleTimeString()}.</h2>    </div>
  );
}

ReactDOM.render(<Clock />, document.getElementById("root"));

We now have a static time stamp.

Side-Effect

Updating the DOM with the date is a side-effect. We will use the useEffect hook to update the date. We now need to define an effect function, which the useEffect calls after every render by default.

  const effect = () => {
    console.log("Effect fired...");
  };

We will pass this function as the first input argument of the useEffect function.

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

function Clock() {
  const initialDate = new Date();
  const [date, setDate] = useState(initialDate);

  const effect = () => {    console.log("Effect fired...");  };  useEffect(effect);
  return (
    <div>
      <h1>Hello, World!</h1>
      <h2>It is {date.toLocaleTimeString()}.</h2>
    </div>
  );
}

ReactDOM.render(<Clock />, document.getElementById("root"));

Let’s open Chrome’s JavaScript Console to see the console.log().

Interval Timer

Now, set up the tick function, which sets the date. Within the effect function, we can now set up an interval timer, that calls this tick function every second.

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

function Clock() {
  const initialDate = new Date();
  const [date, setDate] = useState(initialDate);

  const tick = () => setDate(new Date());
  const effect = () => {
    console.log("Effect fired...");
    setInterval(tick, 1000);  };
  useEffect(effect);

  return (
    <div>
      <h1>Hello, World!</h1>
      <h2>It is {date.toLocaleTimeString()}.</h2>
    </div>
  );
}

ReactDOM.render(<Clock />, document.getElementById("root"));

There is a big problem here…

The useEffect hook, with its current default implementation, calls the effect function after each render. The sequence of events is that the setInterval() function sets up a timer after the first render, which calls the tick function, which in turn sets the date state, which causes a re-render. The re-render causes the effect to be called again, causing another interval timer to be set up, repeating the entire process every second. The main problem is that the number of interval timers set up in the browser grows exponentially, doubling on each re-render.

We can demonstrate this.

The setInterval() function returns an I.D., which we can print out.

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

function Clock() {
  const initialDate = new Date();
  const [date, setDate] = useState(initialDate);

  const tick = () => setDate(new Date());

  const effect = () => {
    console.log("Effect fired...");
    const timerID = setInterval(tick, 1000);    console.log("Create timerID:", timerID);  };
  useEffect(effect);

  return (
    <div>
      <h1>Hello, World!</h1>
      <h2>It is {date.toLocaleTimeString()}.</h2>
    </div>
  );
}

ReactDOM.render(<Clock />, document.getElementById("root"));

Save the file and view the logs in Chrome’s JavaScript Console. This will quickly cause performance issues in your browser.

Let’s comment out the useEffect hook for the time being.

function Clock() {
  const initialDate = new Date();
  const [date, setDate] = useState(initialDate);

  const tick = () => setDate(new Date());

  const effect = () => {
    console.log("Effect fired...");
    const timerID = setInterval(tick, 1000);
    console.log("Create timerID:", timerID);
  };
  // useEffect(effect);
  return (
    <div>
      <h1>Hello, World!</h1>
      <h2>It is {date.toLocaleTimeString()}.</h2>
    </div>
  );
}

Once a setInterval() timer has been set up within the browser after the first render, it will continue to call the tick function every second until the timer is explicitly cleared or the page is reloaded. We do not want to create additional timers on each re-render.

useEffect’s Optional Second Argument

The useEffect hook takes an array as an optional second argument.

Within the array, you can enter any variable in the component scope that you want the useEffect hook to compare between re-renders. You will most likely add a “props” or “state” variable, or even several variables separated by commas, to the array, but any in-scope variable or value is valid.

  • If the value of any variable in the array has not changed between re-renders, then the useEffect hook will not apply the effect.
  • If, however, any value of the array variables has changed, then the useEffect hook will run the effect.

For example, if we add the date state variable to the array, we get the following:

  useEffect(effect, [date]);

We get the same result as before, because the date is updated every second.

As an artificial example, if we pass a constant value instead, which does not change between re-renders, such as the number 7, we get:

  useEffect(effect, [7]);

In Chrome’s Console, you can see that the effect function is not called again after the first render.

You can also pass just the empty array if you do not want useEffect hook to run the effect function on re-renders, no matter which variables within the component are changing or not.

  useEffect(effect, []);

With the empty array [] as the second argument, the useEffect hook only calls the effect after the component is first rendered, that is, when it is “mounted”, and also when the component is removed from the DOM, that is, when it is “unmounted”.

Clean-Up

Now, when the component is unmounted, we want to clean up after ourselves, and remove the interval timer.

The useEffect hook returns a function. The returned function is called after all the effects have run, before the next re-render.

We can clean up after running effects using this returned function, so we will call it cleanup in our example. Now, let’s define the cleanup function to clear the interval timer, and also log it to the console.

  const effect = () => {
    console.log("Effect fired...");
    const timerID = setInterval(tick, 1000);
    console.log("Create timerID:", timerID);
    const cleanup = () => {      console.log("Remove timerID:", timerID);      clearInterval(timerID);    };    return cleanup;  };
  useEffect(effect, []);

Since we are not running the effects after re-renders, we would only clear the interval timer and see the console log when we unmount the component.

We can, however, run the effects whenever the date state variable updates, to see the cleanup function run on each re-render:

  const effect = () => {
    console.log("Effect fired...");
    const timerID = setInterval(tick, 1000);
    console.log("Create timerID:", timerID);
    const cleanup = () => {
      console.log("Remove timerID:", timerID);
      clearInterval(timerID);
    };
    return cleanup;
  };
  useEffect(effect, [date]);

Now we have illustrated our point, we can revert back to the empty array:

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

function Clock() {
  const initialDate = new Date();
  const [date, setDate] = useState(initialDate);

  const tick = () => setDate(new Date());

  const effect = () => {
    console.log("Effect fired...");
    const timerID = setInterval(tick, 1000);
    console.log("Create timerID:", timerID);
    const cleanup = () => {
      console.log("Remove timerID:", timerID);
      clearInterval(timerID);
    };
    return cleanup;
  };
  useEffect(effect, []);
  return (
    <div>
      <h1>Hello, World!</h1>
      <h2>It is {date.toLocaleTimeString()}.</h2>
    </div>
  );
}

ReactDOM.render(<Clock />, document.getElementById("root"));

We have succeeded in properly encapsulating the ticking clock functionality in the Clock component. The Clock component is now more easily reusable.

Summary

We showed you how to encapsulate a component that updates its state. We showed you how to control when the “effect” function is run. And, we showed you how to clean up side-effects when a component is removed from the DOM.

Next Up…

In the next video, we will discuss how data flows in a React application.