react-hook-useeffect

The useEffect Hook in React

  • 5 min

So far, our components have been pure functions: they receive data, render UI, and respond to user events.

But real applications need to do things outside their component world. They need to fetch data from a server, subscribe to keyboard events, manipulate the DOM directly, or set timers.

In functional programming, these operations that occur “outside” the calculation of the function’s result are called Side Effects.

To be able to integrate this type of actions within React’s declarative philosophy, we have the Hook useEffect.

What is useEffect?

useEffect is a Hook that allows us to execute code after React has rendered the component and updated the DOM.

Think of it as the place where we say:

React, once you’re done painting the screen, do this other thing

The basic syntax of useEffect accepts two arguments:

import { useEffect } from 'react';

useEffect(() => {
  // Effect code (API requests, subscriptions...)
}, [/* dependencies */]);
Copied!
  1. A function for the effect (the code to execute).
  2. An array of dependencies (optional, but very important).

The Dependency Array

The most interesting point (and the source of most bugs) of useEffect lies in its second argument. Depending on what we pass in the array, the effect will behave differently.

If we omit the second argument completely, the effect will run after EVERY render.

useEffect(() => {
  console.log("I run every time the component paints");
});
Copied!

Be careful with this. If inside this effect you modify the state (useState), you will cause a new render, which will trigger the effect again, which will change the state… creating an infinite loop that will hang the browser.

If we pass an empty array [], React will only execute it once, right after the first render.

We are basically telling React “This effect does not depend on any props or state values”.

useEffect(() => {
  console.log("I run only once on startup (Mount)");
  
  // Ideal for initial API calls
  fetch('/api/users').then(data => ...);
}, []); // <--- Empty array
Copied!

If we put variables inside (for example, [prop1, prop2]), the effect will run on mount and also every time any of those variables changes value.

const [id, setId] = useState(1);

useEffect(() => {
  console.log(`The ID has changed to ${id}, fetching new data...`);
  // Runs at start and every time 'id' changes
}, [id]);
Copied!

React does a shallow comparison (Object.is) of the dependencies. If the value is different from the previous render, it triggers the effect.

The Cleanup Function

Sometimes, effects create persistent “mess”. For example, if we create a setInterval or subscribe to a global event with addEventListener, those processes will stay alive even if the component disappears from the screen, causing memory leaks.

To avoid this, useEffect allows us to return a cleanup function.

React will execute this function at two times:

  1. Before executing the effect again
  2. When the component unmounts
useEffect(() => {
  // 1. Setup
  const handleResize = () => console.log(window.innerWidth);
  window.addEventListener('resize', handleResize);

  // 2. Cleanup (return function)
  return () => {
    console.log("Cleaning up the event...");
    window.removeEventListener('resize', handleResize);
  };
}, []);
Copied!

Race Conditions

One of the most common problems when fetching data (Data Fetching) inside a useEffect are Race Conditions.

Imagine this scenario:

  1. The user selects “Id 1”, the effect fetches data (takes 3 seconds to arrive).
  2. The user quickly changes to “Id 2”, the effect fetches the data (takes 0.5 seconds to arrive).
  3. The data for Id 2 arrives and is displayed.
  4. The data for Id 1 arrives (because it was slower) and overwrites those of Id 2

The user sees they have selected Id 2, but sees the information for Id 1.

To solve this, we use a pattern with a boolean variable ignore (or cancelled) inside the effect:

useEffect(() => {
  let ignore = false; // Control flag

  async function fetchData() {
    const json = await getData(id);
    // Only update if this effect is still the "valid" one
    if (!ignore) {
      setData(json);
    }
  }

  fetchData();

  // If the component unmounts or the id changes before finishing,
  // we set ignore to true.
  return () => {
    ignore = true;
  };
}, [id]);
Copied!

With this, when “Id 1” finishes loading, its cleanup function will have already executed (because we changed to Id 2), ignore will be true, and the state update will be ignored.

When NOT to use useEffect?

Don’t use it to transform data

If you have firstName and lastName and want fullName, don’t use an effect to do setFullName. Calculate it directly in the component body.

// ❌ Bad
const [firstName, setFirstName] = useState('Luis');
const [lastName, setLastName] = useState('Llamas');
const [fullName, setFullName] = useState('');

useEffect(() => {
  setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);

// ✅ Good (Derived state)
const fullName = firstName + ' ' + lastName;
Copied!
Don’t use it to handle user events

If you want to submit a form on click, do it in the onClick or onSubmit, not in an effect that listens for state changes. The logic should be where the action occurs.