React useEffect Hook - Ultimate Guide

Hooks are special functions that enable the use of state and several other features in functional components available in class-based components.

React useEffect Hook - Ultimate Guide

If you're working with functional components in React, the useEffect hook is an essential tool for managing side effects. Whether you need to perform operations when a component is rendered, updated, or unmounted, the useEffect hook can help you do so with ease.

In functional components, the useEffect hook can be used to perform operations (or side effects) in the following situations:

  • when a component is rendered (componentDidMount function in class-based components)
  • when a component changes (componentDidUpdate function in class-based components)
  • before a component is unmounted/removed (componentWillUnmount function in class-based components)

Why is it called useEffect?

The core React Hooks (useState, useEffect, and so on) were added to the library in 2018. The name of this hook, "useEffect," confused many developers.

What exactly is an "effect"?

In functional programming, the word "effect" refers to a concept known as a "side effect."

But in order to fully understand what a side effect is, we must first understand what a pure function is. Perhaps to your surprise, the majority of React components are designed to be pure functions.

React components may be seen as functions, despite the fact that this may appear strange. It is instructive to note that a standard React function component is declared similarly to a JavaScript function:

function MyReactComponent() {}

The majority of React components are pure functions, which means they take an input and output a predictable output of JSX.

Input to a javascript function is arguments. However, what is a React component's input? Props!

Here, the prop name has been specified on a User component. The prop value is shown in a header element within the User component.

export default function App() {
  return <User name="John Doe" />   
}
  
function User(props) {
  return <h1>{props.name}</h1>;
}

This is a pure function because it produces the same output when given the same input.

Our output will always be John Doe if we provide the User a name prop with the value "John Doe".

The fact that pure functions are dependable, predictable, and easy to test is a huge advantage. This is in contrast to situations where we must implement a side effect in our component.

What are side effects in React?

Side effects are not predictable as actions that are performed with the outside world.

When we need to reach outside of our React components to do something, we perform a side effect. However, performing a side effect will not have a predictable result.

Consider requesting data (such as blog posts) from a server that has failed and returns a 500 status code response instead of our post data.

Common side effects include:

  • Fetching data from an API
  • Reading from local storage
  • Interacting with browser APIs (that is, to use document or window directly)
  • Using unpredictable timing functions like setTimeout or setInterval

This is why useEffect exists: to provide a way to handle performing these side effects in what is otherwise pure React components.

For instance, if we wanted to change the title meta tag to display the user's name in their browser tab, we could do it within the component itself, but we shouldn't.

function User({ name }) {
  document.title = name; 
  // This is a side effect. Don't do this in the component body!
    
  return <h1>{name}</h1>;   
}

The rendering of our React component is affected if a side effect is carried out right in its body.

It is best to keep side effects apart from the rendering process. If we need to do a side effect, it should strictly be done after our component renders.

In a nutshell, useEffect is a tool that allows us to interact with the outside world while preserving the component's rendering and performance.

How do I use useEffect?

UseEffect's basic syntax looks like this:

import { useEffect } from 'react';

function MyComponent() {
  // 2. call it above the returned JSX  
  // 3. pass two arguments to it: a function and an array
  useEffect(() => {}, []);
  
  // return ...
}

The correct way to perform the side effect in our User component is as follows:

  1. We import useEffect from "react"
  2. We call it above the returned JSX in our component
  3. We pass it two arguments: a function and an array
import { useEffect } from 'react';

function User({ name }) {
  useEffect(() => {
    document.title = name;
  }, [name]);
    
  return <h1>{name}</h1>;   
}

useEffect receives a callback function as its parameter. After the component renders, this will be invoked.

We can perform one or more side effects in this function if we want.

The dependencies array is the second parameter, which is an array. All the values on which our side effect depends should be included in this array.

We need to include name in the dependents array in the example above because we are changing the title depending on a value in the outer scope.

This array will check to see whether a value (in this case, name) has changed between renderings. If so, our useEffect function will be executed again.

This makes sense since, in case the name changes, we want to display the updated name and rerun our side effect.

What is the cleanup function in useEffect?

The effect cleanup function is the last step in carrying out side effects correctly in React.

Our side effects occasionally need to be turned off. For instance, if you use the setInterval method to start a countdown timer, that interval won't end until we use the clearInterval function. We use the cleanup function for this.

If we use setInterval to set the state and that side effect is not cleaned up when our component unmounts and we no longer use it, the state is destroyed along with the component - but the setInterval method continues to run.

function Timer() {
  const [time, setTime] = useState(0);
    
  useEffect(() => {
    setInterval(() => setTime(1), 1000);
  }, []);
}

The problem with the above code is that when the component is destroyed, setInterval will try to update a variable of the state time that no longer exists. This is an error called a memory leak.

We need to stop using setInterval when the component unmounts.

function Timer() {
  const [time, setTime] = useState(0);

  useEffect(() => {
    let interval = setInterval(() => setTime(1), 1000);

    return () => {
      clearInterval(interval);
    };
  }, []);
}

We can perform our cleanup within this function by calling clearInterval.

The cleanup function will be called when the component is unmounted.

Going to a new page or route in your application where the component is no longer displayed is a common example of a component being unmounted.

When we unmount a component, our cleanup code runs, our interval is cleared, and we no longer get an error about trying to update a state variable that doesn't exist.

Last but not least, cleaning up after side effects is not always necessary. It is only necessary for very few circumstances, such as when you want to stop a recurring side effect after your component unmounts.

The importance of the dependency array

Let's take a look at the below example with two states. Why do we have the problem of unnecessary effects?

export default function TwoStatesEffects() {
  const [title, setTitle] = useState('default title');
  const titleRef = useRef();
  const [darkMode, setDarkMode] = useState(false);

  useEffect(() => {
    console.log('useEffect');
    document.title = title;
  });
  console.log('render');
  const handleClick = () => setTitle(titleRef.current.value);
  const handleCheckboxChange = () => setDarkMode((prev) => !prev);

  return (
    <div className={darkMode ? 'dark-mode' : ''}>
      <label htmlFor="darkMode">dark mode</label>
      <input
        name="darkMode"
        type="checkbox"
        checked={darkMode}
        onChange={handleCheckboxChange}
      />
      <input ref={titleRef} />
      <button onClick={handleClick}>change title</button>
    </div>
  );
}
Two states causing unnecessary effects
Two states causing unnecessary effects

If you do not provide a dependency array, each useEffect is executed after every render cycle. In our case, whenever one of the two-state variable change, the useEffect is executed.

Dependencies that you supply as array items are used to handle this behavior. React will only execute the useEffect statement if at least one of the supplied dependencies has changed since the last run. To put it another way, you can condition the execution using the dependency array.

We want to skip unnecessary effects after an intended re-render, right?

All we need to do is add an array with the title as a dependency. As a result, the effect is only executed when values between render cycles vary.

 useEffect(() => {
    console.log('useEffect');
    document.title = title;
  }, [title]);
skip-unnecessary-effect
skip unnecessary effect

We can also add an empty dependency array which will execute the effect only once.

How to fix common mistakes with useEffect

With useEffect, a few important things to be aware of in order to avoid mistakes.

useEffect(() => {
  //the code here runs every time the screen is rendered
});

If you do not provide any dependency array to the useEffect, it will run after every render.

If you set a local state variable when the state is changed and useEffect runs without the dependencies array after each render, the loop will continue indefinitely.

function MyCustomer() {
  const [customer, setCustomer] = useState([]);

  useEffect(() => {
    fetchCustomer().then((myCust) => setCustomer(myCust));
    // Error! useEffect runs after every render without the dependencies array, causing infinite loop
  });
}

After the first render, useEffect is executed, the state is changed, which causes a re-render, which triggers useEffect to execute once again, and so on indefinitely.

To fix the infinite loop, we should pass an empty dependencies array. This will cause the effect function to only run once after the component has been rendered the first time.

function MyCustomer() {
  const [customer, setCustomer] = useState([]);

  useEffect(() => {
    fetchCustomer().then((myCust) => setCustomer(myCust));
  }, []);
}

The rules of Hooks

Let's talk about the general rules of Hooks. Although they are not restricted to the useEffect Hook, it's important to know where you can define effects in your code. You need to follow the rules to use Hooks.

  1. Only the top-level function of your functional React component can call hooks.
  2. Don't call hooks inside loops, conditions, or nested functions.

Conclusion

I believe that one of the most important skills to master if you want to become a next-level React developer is understanding the underlying design concepts and best practices of the useEffect Hook.