Let's talk about why you need useEffect!

useEffect is a Hook provided by React for handling side effects. For example, sending requests, manipulating the DOM, and setting listeners are all side effects.

A side effect is an effect that occurs outside of returning a value during the execution of a function

Understanding Existing Problems

So what is the relationship between useEffect and side effects? What problems arise if side effects are written directly in components? Understanding the existing problems is essential to better explain why the useEffect Hook is needed.

Problem 1: Impure components lead to unstable code

The goal of avoiding side effects in functional components is to prevent instability in the program. You wouldn’t want different results every time the component is rendered, as this makes the program unpredictable and hard to maintain. In the following example, every time the Cup function is rendered, the guest variable will be modified.

let guest = 0;
function Cup() {
// Bad practice: modifying a variable outside the function, causing side effects!
guest = guest + 1;
return <h2>Tea for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup />
<Cup />
<Cup />
</>
);
}

The result is that the program becomes difficult to maintain because it is unclear what result will be obtained each time the function is called. Defining variables as global variables is also a very bad practice.

Problem 2: Component re-renders cause side effects to execute repeatedly

Rendering of components must be separated from side effects! Otherwise, every time the component re-renders, the side effects will execute, leading to unnecessary performance waste. For example, if the program frequently re-renders, the rendering will also execute the side effect code, which may cause the user to fill in an input box, but due to changes in State, a re-render occurs, causing the request (side effect) to be executed excessively!

export function App(props) {
const [input, setInput] = useState('');
function handleChange(e) {
setInput(e.target.value);
}
// Bad practice: side effects will execute whenever the component renders!
fetch('https://picsum.photos/200/300');
return (
<div className="App">
<input type="text" onChange={handleChange} />
</div>
);
}

In the above example, every time a character is changed in the input box, a request will be sent ☹.

Using useEffect

After understanding the context, let’s get to the point: how can useEffect solve these problems? It can be broken down into three steps:

  1. Declare a side effect function: The side effect function within the component will execute during the component’s rendering.
  2. Specify the dependencies of the side effect: To avoid executing the side effect on every render, the dependencies of the side effect should be specified, so that the side effect only executes when the dependencies change.
  3. Optionally add a cleanup function: Some side effects need to be cleaned up when the component is removed. For example, when binding event listeners, the listeners should be removed when the component is removed, and the removal behavior can be written in the cleanup function.

The above is a general concept. Let’s try to write it using the useEffect Hook. The overall structure is as follows:

import { useEffect } from 'react';
useEffect(() => {
// Side effect
return () => {
// Cleanup function
};
}, [dependencies]);

Step 1: Passing in Side Effects

The most basic usage is to pass in a function that executes side effects, but useEffect does not know when to re-execute the side effect. Therefore, in the following example, the side effect will still be executed with the rendering of the component.

useEffect(() => {
console.log('Execute side effect whenever the component renders');
});

Step 2: Specifying Dependencies

We do not want the side effect to execute every time the component re-renders. Therefore, we can specify an array of dependencies for the side effect, which will only execute when the dependencies change. In other words, if we want the side effect to execute only on the first render of the component, we can pass in an empty array.

useEffect(() => {
console.log('Will only execute on the first render of the component');
}, []);

We can also pass in dependencies, and the side effect will only execute when that dependency changes. For example, if we want the side effect to execute only when name changes, we can pass name as a dependency.

useEffect(() => {
document.title = `Hello! Visitor: ${name}`;
}, [name]);

Step 3: Cleanup Function

The cleanup function is actually a function returned from the side effect function, which will be executed when the component is removed or re-rendered. Not all side effects require a cleanup function; it can be added as needed. Specifically, we can think about what impact re-executing the side effect would have, which will naturally indicate whether a cleanup function is needed. A common example is if the side effect binds an event listener; when the component is removed, the listener should also be removed. Otherwise, each time the component re-renders, a new listener will be bound, causing performance issues.

useEffect(() => {
const handler = () => {
console.log('Mouse moved');
};
window.addEventListener('mousemove', handler);
return () => {
window.removeEventListener('mousemove', handler);
};
}, []);

In the above example, the event listener is only bound to window on the first render of the component. If this component is destroyed or re-rendered, the cleanup function will be executed first.

Summary

To understand useEffect, one must have a certain understanding of the basic underlying mechanisms of React. Through useEffect, side effects can be separated from component rendering, avoiding unnecessary performance waste and instability issues in the program.

References