Get Started with Modern React: Step by Step
S02・V09: State (Part 2)
- We will show you how to encapsulate a component that updates its state.
- We will show you how to control when the “effect” function is run.
- We will show you how to clean up side-effects when a component is removed from the DOM.
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 theeffect
. - If, however, any value of the array variables has changed, then the
useEffect
hook will run theeffect
.
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.