Module 3 - Composing Side Effects
The effect hook
It's time to learn about the world of Side Effects in React!
"Side Effects" refer to actions that execute when something changes in the scope of a component. That's a very generic statement, but that's precisely the point of side effects. For instance, you can run code conditionally if a particular state is updated or any of the received props changes.
Some examples of common side effects would be network requests, logging, initiating a timer, or performing some calculations.
By using the useEffect() hook, your application will be able to reach new levels of responsiveness and implement complex behaviors. Let's dive in!
Working with side effects
Let's first revisit the concept of a React component's lifecycle.
Component Lifecycle
When React renders a component in the DOM, the component is said to be mounted.
From then on, a component will enter the update phase. It will be executed or "rendered" (although the latter term might suggest a DOM render) whenever it is called by a parent component, with or without props, and whenever its local state changes.
For instance, a parent component might call a child component with props, or an onClick callback might change the component's state. These events will cause the component function to run and compute a new JSX output. React then compares the output with the browser's DOM to determine if an update is needed. If a component's new JSX matches what is currently on the DOM, React will not update the DOM. However, if the new JSX is different, React will update the DOM with the new content. React can improve the application's responsiveness and save computing power by only making necessary DOM updates.
Finally, when React removes a component from the DOM, the component is said to be unmounted.
Side Effects and the Effect Hook
Now that you better understand the component's lifecycle, you will learn how to fit effects in the lifecycle context and decide when to run them.
The useEffect hook tells React that a component needs to run some side effect when a particular condition occurs. This hook is declared inside the component function, usually right after the state/props declarations, and always outside the return statement (JSX).
The argument of the hook is the callback function with the actual side effect code. To set up an effect, you first import the hook:
import {useEffect} from 'react';
And then pass the callback to the hook (arrow syntax used here):
useEffect( () => {} );
Using this basic declaration above, you will set the effect to run whenever any of the component's props or state is updated and only if they are updated.
This is how it works:
- After the initial render and subsequent updates, React creates a snapshot of the component's state, props, and other variables within its scope.
- After a re-render is triggered, React compares the new snapshot of the component's state, props, and other variables with the previous snapshot that was stored.
- When the component is executed again, React will compare the current snapshot with the previous and check if there's any change.
- If React detects that any of the values in the snapshot have changed since the last render, it will execute the callback function specified in the useEffect hook.
Pro Tip: Most of the time, changes in props or state inside a component will cause React to update the real DOM, so that the new data is shown somewhere in the user interface. Effects run after any changes to the DOM are finished. However, effects will execute even if React determines that no changes to the DOM are necessary.
How to Build It
Let's see an example of how useEffect works. We'll modify our ScoreBoard component from the previous module:
import React, {useEffect} from 'react';
export default function ScoreBoard(props) {
const {score, player} = props;
useEffect( () => {
console.log("The component has updated.");
});
return (
<div>
<h3>
{player} is at {score}
</h3>
</div>
)
};
Run the app, and open the dev tools console. You will see the initial log message, which happened right after the component was mounted. That's because the previous component's snapshot was non-existing, so there has been a change! That's why side effects always run upon mounting.
Click on the Decrease button. Nothing happens! That's because the score prop is not changing; all the props remain the same. Now click on Increase, and you will get another log message!
Since the hook is in the same function scope, it can also access props and state. Replace the useEffect hook with this one:
useEffect( () => {
console.log("The new score is", score);
});
By the way, you don't use brackets here to access score because this is pure JS, not JSX.
Under the hood, this is the order of events in React:
- Component is mounted, scope changes, so the callback function is registered to be executed later;
- The JSX output is calculated;
- React updates the DOM;
- The callback function is executed (message logged);
- You click on Decrease;
- Scope does not change, the component is not executed.
- You click on Increase;
- Scope changes (score prop changes), the component is executed;
- The effect (callback) is placed in the execution queue;
- The JSX output is calculated;
- The JSX output is different from the previous, React updates the DOM with the new content;
- The effect is executed (message logged);
- etc.
Next, you will learn how to selectively trigger the side effect!
Triggering side effects
The actual effects of side effects and when they are supposed to run are entirely up to your application design. By default, the callback passed to useEffect() will run every time the component's scope is changed. But side effects can be configured to run selectively in different ways:
- Just once after the component is mounted, and never again.
- Anytime there's an update, including on mount (default).
- When there are updates related only to specific props or state
- And optionally run additional clean-up code when the component is unmounted (more on this later).
By carefully designing when to run your side effects, you can save computing resources, improve the user experience, and avoid infamous infinite loops! Let's talk more about infinite loops.
Consider this situation: your component executes, a side effect is triggered, and the effect effectively changes the state. React will notice the state change and rerun the component. The component will trigger the side effect, and the process starts again. You just got an infinite loop that will crash the browser.
To avoid infinite loops, you should follow some general rules regarding side effects:
- Avoid unconditionally changing state inside side effects
- Avoid the default behavior and configure side effects to be triggered only when specific props or state are changed
- Consider if a particular effect should run only once upon mounting.
How to Build It
Let's see how you can change the default behavior of useEffect(). You do this by passing in a dependency array as a second argument to the hook:
useEffect( () => {}, [dependencies] );
In this first example below, we are passing an empty dependency array to useEffect(), which configures the effect to run only once upon mounting. Now the ScoreBoard component can be updated:
import React, {useEffect} from 'react';
export default function ScoreBoard(props) {
const {score, player} = props;
useEffect( () => {
console.log("The component has mounted.");
}, []);
return (
<div>
<h3>
{player} is at {score}
</h3>
</div>
)
};
If you check the console output while you click on the buttons, you will see only one log message which took place upon mounting. Technically, the callback was registered to be executed later. The component rendered the DOM, then the callback was executed, and the message was logged.
Let's add some more effects. Update the ScoreBoard.js file with this code:
import React, {useEffect} from 'react';
export default function ScoreBoard(props) {
const {score, player} = props;
useEffect( () => {
console.log("The component has mounted.");
}, []);
useEffect( () => {
console.log("The new score is", score);
}, [score]);
useEffect( () => {
console.log("The new player is", player);
}, [player]);
return (
<div>
<h3>
{player} is at {score}
</h3>
</div>
)
};
Again, check the console output while you click on the buttons. What's happening here?
We have added three side effects to our component. As you know, the first one is triggered only upon mounting (empty dependency array). The second one has [score] as the dependency array, which sets the effect to run whenever the score prop changes, ignoring any other props or state. The last one will trigger the effect only when the player prop changes, which never happens in our current app, except for the initial mounting.
In summary, every effect will trigger at least on mounting and subsequently whenever any of the state/props in the dependency array changes. These are the possibilities:
useEffect(fn);
fn executed on mounting and upon changes in any state/props.
useEffect(fn, []);
fn executed on mounting only.
useEffect(fn, [state1, state2, prop1]);
fn executed on mounting and upon changes in either state1, state2, or prop1.
Pro Tip: In this latter case, changes in any other state/props will run the component function (and possibly update the DOM) but will not trigger the effect.
Fetching data on component mount
One everyday use for side effects is to fetch data from remote servers. Applications often rely on retrieving up-to-date data from the web, either from third-party public services or private services designed to be accessed by internal clients.
For instance, the app might need to get current weather information for a particular city and update that information on the screen every 5 minutes. Another app used by a company's employees may need to access a private database to get contact information for clients whenever the user clicks on a particular button. These are perfect scenarios for using side effects!
All these data can be exposed as web service APIs, making it easy for client-side code to get what it needs when it needs it. Many web APIs return data in the JSON format, which is considerably more efficient than formats like XML, and is easily manipulated in JavaScript. In the examples below, you will be using the more powerful axios JS module for fetching JSON data, as opposed to the native fetch() JS method.
By the way, web APIs usually are not restricted to certain types of clients. The same information you can get in your React app is also available for other clients running Python, for instance, as long as they all have authorization to access the service.
How to Build It
For the web service, you will be using a local server containing some sample data. The API will be exposed at port 9009, and is part of the same Bloomtech's React bootstrapping tool version 0.1.6
Go ahead and initiate a new project with:
npx @bloomtools/react@0.1.6 pics
cd pics
npm install
npm run dev
The project has Axios installed already. If it didn't, you would run the following command in the same directory:
npm install axios
Fetching Data when a Component Mounts
For this first example, you will fetch a random image when the component mounts and render it to the DOM. The API in the local development server will return a JSON object containing the url and any details about a random picture. Make sure the API is working by testing:
http://localhost:9009/api/pics/random
And you should get something like this:
{"file":"https://bloominstituteoftechnology.github.io/img/karthik-sreenivas-rsx-joaKYrk-unsplash.jpg","detail":"Photo by Karthik Sreenivas on Unsplash"}
The JSON object returned by the server contains two keys: file with the actual URL and detail with the license attribution information.
The first thing we'll do is get our App component ready. You will create some state, declare an empty effect hook and format the proper JSX. Edit the App.js file:
import React, {useState, useEffect} from 'react';
export default function App() {
// Initialize state to hold the image URL
const [pic, setPic] = useState("");
useEffect(() => {}, []); // Not synced with any data, so this effect only fires once
return (
<div className="App">
<h1>This is a nice random picture!</h1>
<img src={pic} alt="a random picture" />
</div>
);
};
Now we can add the fetch logic. You will add another state and JSX element for the detail value. We'll use the Axios module here:
import React, {useState, useEffect} from 'react';
import axios from 'axios';
export default function App() {
// Initialize state to hold the image URL
const [pic, setPic] = useState("");
// Initialize state to hold the image author
const [info, setInfo] = useState("");
useEffect( () => {
axios
.get("http://localhost:9009/api/pics/random")
// Which we then set to state
.then(res => {
setPic(res.data.file);
setInfo(res.data.detail)
})
// Always include error handling
.catch(err => console.log(err));
}, []); // Not synced with any data, so this effect only fires once
return (
<div className="App">
<h1>This is a nice random picture!</h1>
<img src={pic} height="300px" alt="a random picture" />
<h2>{info}</h2>
</div>
);
};
Wait! You just learned that we should avoid changing state inside the effect! And that's exactly what's happening here: the effect is calling setPic() and setInfo. Well, fetching data always means saving the data, so the effect must update state. Since the state is changing, you have to be very careful here about infinite loops. One solution is to set the effect to run only once on mounting by setting the last argument of useEffect as an empty array (which is what the code is doing). Another solution would be to use the dependency array to monitor state other than pic and info.
Pro Tip: when designing and testing components, add a console.log statement at the beginning of the function so you know exactly how many times the component was fired:
import React, {useState, useEffect} from 'react';
import axios from 'axios';
export default function App() {
console.log("App component fired");
const [pic, setPic] = useState("");
.
.
.
}
In our case, if you inspect the browser's console, you will notice that App is running twice upon mounting. Technically, this is what's happening:
- App updates the DOM with an empty picture and empty info.
- The effect is triggered
- App state is changed
- React notices the state change and fires App again
- App updates the DOM with the picture and info.
There's not much you can do to avoid this double-run. Just keep that in mind and always inspect the console to check if your component is running much more often than you would expect. In most cases, the solution will be changing the dependency array.
Cleaning up on component unmount
Imagine a component that subscribes to an API on mounting to receive live updates from the Stock Market. When this component is no longer needed by the application and is eventually unmounted, the subscription should be canceled. Otherwise, your application might waste resources or create memory leaks and overflows.
This situation is not unique to React. Any application that deals with subscriptions, event listeners, and other pieces of code that are constantly waiting for live events must have a way to clean up resources and "kill" processes that are no longer needed. This design pattern of cleaning up unneeded resources avoids security and performance issues. React will handle unmounting components that are no longer needed, but you will be responsible for cleaning up any resources used in any effects you created.
To clean up any resources used in an effect in React, you can return a function from useEffect like this:
useEffect(() => {
// We write our desired effect as always.
console.log("The effect has run.");
// Returning a function will tell React that you want this
// code to run when the component unmounts
return () => console.log("This code runs when the component is unmounted");
});
The syntax above tells React that there's some extra code that must be saved for later and that this code should be executed when the component unmounts. This is the perfect time to clean up any resources you want to remove.
How to Build It
Let's edit the App component and add an event listener to the DOM:
import React, {useState, useEffect} from 'react';
export default function App() {
console.log("App component has run.");
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = e => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener("mousemove", handleMouseMove);
console.log("Effect has run, event listener added.");
return () => {
console.log("App component unmounted, effect has removed the event listener.");
window.removeEventListener("mousemove", handleMouseMove);
};
}, []);
return (
<div>
<h3>Mouse position:</h3>
{position.x}:{position.y}
</div>
);
};
The effect is triggered only once on mounting. From that point, the event listener will take care of updating the state of the component. You can verify this behavior by inspecting the console.
Pro Tip: when declaring auxiliary functions like handleMouseMove above, always do it inside the useEffect() hook to guarantee that all state and order of execution will be synchronized in React.
Now try removing the dependency array, refreshing the app, and checking the console. The effect is now triggering for every mouse move, which is not desired nor necessary. But React is preventing you from creating a mess by assuming that any clean-up code declared in the effect should be executed between effect runs.
In other words, the clean-up code is executed not only when the component is unmounted but also when the effect is re-triggered.
Conclusion
A well-designed React application should use effects to implement most of its core logic. Effects can be perfectly synchronized with user interactions and DOM rendering as long as you understand the order of execution and the effect's dependencies in React. When you first begin to code effects, sooner or later, you will run into infinite loops and unexpected behaviors. Use the console to verify what's happening, follow the golden rules of React, and you will become an Effects Master in no time!
Module 3 Project: Composing React Components
This project will be used for both module 3 and module 4. In this project, you will build an application to show the NASA photo of the day. You will start from scratch and build the entire app. You don't have any design specs to follow for this project, so start by making simple drawings that outline how your app will look. These simple drawings are called "wireframes" and it's as simple as using a pencil and drawing some boxes. Make the wireframe simple initially since you don't know what data you'll get back from NASA.
When you do get the data back, there may be more or less than you expected, so your design plans may change. That's totally fine and very normal in the real world. Just make the proper adjustments and move forward!
This is a really fun project, and one to show your family and friends when you've finished. Have some fun with it!
The module project contains advanced problems that will challenge and stretch your understanding of the module's content. The project has built-in tests for you to check your work, and the solution video is available in case you need help or want to see how we solved each challenge, but remember, there is always more than one way to solve a problem. Before reviewing the solution video, be sure to attempt the project and try solving the challenges yourself.
Instructions
The link below takes you to Bloom's code repository of the assignment. You'll need to fork the repo to your own GitHub account, and clone it down to your computer:
Starter Repo: Nasa Photo of the Day
- Fork the repository,
- clone it to your machine, and
- open the README.md file in VSCode, where you will find instructions on completing this Project.
- submit your completed project to the BloomTech Portal