preloader

Intro

When React introduced Hooks in version 16.8, we all eagerly reached for them. Lifecycle methods could now be forgotten and replaced with hooks! The useEffect hook is one of the most used and is a bit more complex than others. It allows you to add side-effects (e.g., do stuff after the initial render) and can act upon the updates of the state. These use cases might sound trivial, but many things can and often do go wrong!  

This article will discuss the dos and don’ts for the `useEffect` hook. 

Short Brush-Up on `useEffect` 

This hook is a function which receives a function whose body executes on both the initial render of a component and when any of the dependencies (from the dependency list) change. The hook can return a cleanup function, which is run directly before the side-effect is run (if the side-effect has occurred at least once). Also, the cleanup is run when a component unmounts. 

The following sections will discuss some of the common mistakes with the `useEffect` hook. 

I – You Might Not Need useEffect in the First Place 

Let’s consider the following example: 

function MyComponent() { 
    const [firstName, setFirstName] = useState(“”); 
    const [lastName, setLastName] = useState(“”); 
    const [fullName, setFullName] = useState(“”); 
    useEffect(() => { 
      setFullName(`${firstName} ${lastName}`); 
    }, [firstName, lastName]); 
    // rendered inputs to change first and last name 
} 

This will work fine, but it’s not an optimal solution. Every time our first or last name changes, we update the `fullName`. As a result, our component re-renders twice: whenever either the first or last name changes (once for the first/last name and once more for `fullName`). Since our component re-renders every time, we don’t need ‘useEffect’ or the separate state for `fullName`. Instead, we can just say: 

const fullName = `${firstName} ${lastName}`; 

II – JavaScript Uses Referential Equality (So Does Your Dependency Array)

JavaScript compares primitive types (e.g., string and numbers) by value and complex types (e.g., objects and arrays) by references to their memory locations. If we don’t pay attention to this, `useEffect’ could run unexpectedly.  

Here’s an example: 

function MyComponent() { 
    const [name, setName] = useState(“John Doe”); 
    const [age, setAge] = useState(25); 
    const [darkMode, setDarkMode] = useState(false); 
    const person = { age, name }; 
    useEffect(() => { 
      console.log(person); 
    }, [person]); 
    // rendered inputs to change name, age and darkMode 
}

We would expect our `useEffect` to run every time the `person` object changes. Since our `person` only contains `age` and `name`, our effect should run whenever any of those two changes. This is true, but our hook runs when we toggle `darkMode`. Why is that so? 

Every time we update the `darkMode`, the component re-renders, which creates a new `person` object. Since JavaScript compares objects by reference, our `person` object seems “new” to the effect and it is run every time. 

We can fix this with a `useMemo` hook on the `person` object which makes sure that the object is changed only when either `age` or `name` change. It would look something like this: 

const person = useMemo(() => ({ age, name}) , [age, name]); 

III – Cleanup Functions – Aborting Requests 

The cleanup functions should be used to ensure that our effect runs in a clean environment, unaffected by the previous effect runs.   

A common use of `useEffect` hook is to fetch some data. This can be done using a simple `fetch` call, like this: 

useEffect(() => {  
    fetch(url})  
      .then(setData)  
      .catch(setError); 
  }, [url]);

At first, this looks fine. But what happens when url changes multiple times in one second and we have a slow or overloaded backend which takes more than one second to return our data? Also, what happens when we send out 3 requests and the responses come back in the wrong order? 

This can happen and we will end up having a component with inconsistent/unexpected data. To fix this, we should use the cleanup part of `useEffect` hook to abort the fetch request. This can be done using the `AbortController`, like this: 

useEffect(() => {  
    const controller = new AbortController();  
    fetch(url, { signal: controller.signal })  
      .then(setData)  
      .catch(setError)  
    return () => {  
      controller.abort();  
    }  
  }, [url]);

Here, `AbortController` allows us to use a simple `signal` to notify the current `fetch` that its data is not relevant anymore. We trigger this signal in the cleanup function, which (as we said in the intro) is run directly before the side effect is run (considering that the side effect has occurred at least once). 

Conclusion 

To sum up the points presented above: 

  1. Think before you reach for `useEffect`. 
  1. Check your dependency array – pay attention to your objects/arrays or just try to depend primarily on the primitive types. 
  1. Clean up after yourself! 

As you can see, ‘useEffect’ is a powerful hook with many use cases. However, we need to use it in proper contexts for adequate goals. The guide above is here to help you get familiar with the three common pitfalls of ‘useEffect’ and utilize it for the best possible effect.  

About Author

Filip Savic – guitar-playing and dragon-slaying frontend-focused software engineer with over five years of experience creating cutting-edge web and mobile apps. Whether in the Healthcare, Entertainment, or Crypto space, Filip’s apps have consistently driven exponential growth of his customers’ user bases. His passion for innovation has led him to take on challenging projects like developing media-sharing and AI-NFT platforms from scratch while focusing on crafting simple and intuitive user interfaces.