When Does A useEffect Hook Run

The useEffect hook is one of the most important hooks in React, allowing you to perform side effects in function components. Understanding when it runs is crucial for writing effective React applications.

Understanding the Problem

We need to create a React component that demonstrates different behaviors of the useEffect hook based on:

Devising a Plan

  1. Create a UseEffectTest component
  2. Add a useEffect with no dependency array
  3. Add state and a button to trigger re-renders
  4. Modify the useEffect to use an empty dependency array
  5. Add a second state variable and useEffect with that variable in the dependency array
  6. Add conditional logic inside the second useEffect
  7. Add a counter and a third useEffect with a cleanup function
  8. Update App.jsx to render our component

Carrying Out the Plan

Step 1: Create UseEffectTest.jsx

First, let's create our basic component structure:

// src/UseEffectTest.jsx

const UseEffectTest = () => {
  return (
    <div>
      <h1>UseEffectTest Component</h1>
    </div>
  );
};

export default UseEffectTest;

Step 2: Update App.jsx

Next, let's update the App.jsx file to use our new component:

// src/App.jsx

import UseEffectTest from './UseEffectTest';

function App() {
  return (
    <UseEffectTest />
  );
}

export default App;

Step 3: Add useEffect with no dependency array

Let's add our first useEffect without a dependency array:

// src/UseEffectTest.jsx
import { useEffect } from 'react';

const UseEffectTest = () => {
  // This useEffect will run after every render
  useEffect(() => {
    console.log('UseEffect1 Ran');
  }); // No dependency array

  return (
    <div>
      {console.log('rendered or re-rendered')}
      <h1>UseEffectTest Component</h1>
    </div>
  );
};

export default UseEffectTest;

Step 4: Add state and button to trigger re-renders

Now we'll add a state variable and button to test re-rendering:

// src/UseEffectTest.jsx
import { useEffect, useState } from 'react';

const UseEffectTest = () => {
  // Add state to trigger re-renders
  const [toggleOne, setToggleOne] = useState(false);

  // This useEffect will run after every render
  useEffect(() => {
    console.log('UseEffect1 Ran');
  }); // No dependency array

  return (
    <div>
      {console.log('rendered or re-rendered')}
      <h1>UseEffectTest Component</h1>
      <button onClick={() => setToggleOne(!toggleOne)}>ToggleOne</button>
    </div>
  );
};

export default UseEffectTest;

Step 5: Modify useEffect to use an empty dependency array

Now we'll add an empty dependency array to our useEffect:

// src/UseEffectTest.jsx
import { useEffect, useState } from 'react';

const UseEffectTest = () => {
  // Add state to trigger re-renders
  const [toggleOne, setToggleOne] = useState(false);

  // This useEffect will run only after the first render
  useEffect(() => {
    console.log('UseEffect1 Ran');
  }, []); // Empty dependency array

  return (
    <div>
      {console.log('rendered or re-rendered')}
      <h1>UseEffectTest Component</h1>
      <button onClick={() => setToggleOne(!toggleOne)}>ToggleOne</button>
    </div>
  );
};

export default UseEffectTest;

Step 6: Add a second state variable and useEffect

Let's add another state variable and another useEffect that depends on it:

// src/UseEffectTest.jsx
import { useEffect, useState } from 'react';

const UseEffectTest = () => {
  // Add state to trigger re-renders
  const [toggleOne, setToggleOne] = useState(false);
  const [toggleTwo, setToggleTwo] = useState(false);

  // This useEffect will run only after the first render
  useEffect(() => {
    console.log('UseEffect1 Ran');
  }, []); // Empty dependency array

  // This useEffect will run after first render and whenever toggleTwo changes
  useEffect(() => {
    console.log('UseEffect2 Ran');
  }, [toggleTwo]); // toggleTwo in dependency array

  return (
    <div>
      {console.log('rendered or re-rendered')}
      <h1>UseEffectTest Component</h1>
      <button onClick={() => setToggleOne(!toggleOne)}>ToggleOne</button>
      <button onClick={() => setToggleTwo(!toggleTwo)}>ToggleTwo</button>
    </div>
  );
};

export default UseEffectTest;

Step 7: Add conditional logic inside useEffect

Now let's add conditional logic inside our second useEffect:

// src/UseEffectTest.jsx
import { useEffect, useState } from 'react';

const UseEffectTest = () => {
  // Add state to trigger re-renders
  const [toggleOne, setToggleOne] = useState(false);
  const [toggleTwo, setToggleTwo] = useState(false);

  // This useEffect will run only after the first render
  useEffect(() => {
    console.log('UseEffect1 Ran');
  }, []); // Empty dependency array

  // This useEffect will run after first render and whenever toggleTwo changes
  useEffect(() => {
    console.log('UseEffect2 Ran');
    // Conditional logic inside useEffect
    if (toggleTwo) {
      console.log('toggleTwo slice of state is true so this code runs');
    }
  }, [toggleTwo]); // toggleTwo in dependency array

  return (
    <div>
      {console.log('rendered or re-rendered')}
      <h1>UseEffectTest Component</h1>
      <button onClick={() => setToggleOne(!toggleOne)}>ToggleOne</button>
      <button onClick={() => setToggleTwo(!toggleTwo)}>ToggleTwo</button>
    </div>
  );
};

export default UseEffectTest;

Step 8: Add counter and useEffect with cleanup function

Finally, let's add a counter and a third useEffect with a cleanup function:

// src/UseEffectTest.jsx
import { useEffect, useState } from 'react';

const UseEffectTest = () => {
  // Add state to trigger re-renders
  const [toggleOne, setToggleOne] = useState(false);
  const [toggleTwo, setToggleTwo] = useState(false);
  const [count, setCount] = useState(0);

  // This useEffect will run only after the first render
  useEffect(() => {
    console.log('UseEffect1 Ran');
  }, []); // Empty dependency array

  // This useEffect will run after first render and whenever toggleTwo changes
  useEffect(() => {
    console.log('UseEffect2 Ran');
    // Conditional logic inside useEffect
    if (toggleTwo) {
      console.log('toggleTwo slice of state is true so this code runs');
    }
  }, [toggleTwo]); // toggleTwo in dependency array

  // This useEffect demonstrates the cleanup function
  useEffect(() => {
    // Create an interval that logs a message every second
    const myInterval = setInterval(() => {
      console.log(`UseEffect3 with interval number ${count} is running`);
    }, 1000);

    // Return a cleanup function that will run before the next effect execution
    // or when the component unmounts
    return () => {
      console.log(
        `UseEffect3 cleanup ran.\nsetInterval number ${count} is being cleared out`
      );
      clearInterval(myInterval);
    };
  }, [count]); // count in dependency array

  return (
    <div>
      {console.log('rendered or re-rendered')}
      <h1>UseEffectTest Component</h1>
      <button onClick={() => setToggleOne(!toggleOne)}>ToggleOne</button>
      <button onClick={() => setToggleTwo(!toggleTwo)}>ToggleTwo</button>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </div>
  );
};

export default UseEffectTest;

Looking Back

Let's review what we've learned about the useEffect hook:

When does useEffect run?

Conditional Logic in useEffect

You can use conditional statements inside useEffect to run certain code only when specific conditions are met. However, you cannot use conditionals to determine whether the useEffect itself runs - it will always run when its dependencies change.

Cleanup Function

The cleanup function:

Real World Examples

Some common use cases for useEffect:

Final Component

Here's the complete UseEffectTest.jsx file:

// src/UseEffectTest.jsx
import { useEffect, useState } from 'react';

const UseEffectTest = () => {
  // Add state to trigger re-renders
  const [toggleOne, setToggleOne] = useState(false);
  const [toggleTwo, setToggleTwo] = useState(false);
  const [count, setCount] = useState(0);

  // This useEffect will run only after the first render
  useEffect(() => {
    console.log('UseEffect1 Ran');
  }, []); // Empty dependency array

  // This useEffect will run after first render and whenever toggleTwo changes
  useEffect(() => {
    console.log('UseEffect2 Ran');
    // Conditional logic inside useEffect
    if (toggleTwo) {
      console.log('toggleTwo slice of state is true so this code runs');
    }
  }, [toggleTwo]); // toggleTwo in dependency array

  // This useEffect demonstrates the cleanup function
  useEffect(() => {
    // Create an interval that logs a message every second
    const myInterval = setInterval(() => {
      console.log(`UseEffect3 with interval number ${count} is running`);
    }, 1000);

    // Return a cleanup function that will run before the next effect execution
    // or when the component unmounts
    return () => {
      console.log(
        `UseEffect3 cleanup ran.\nsetInterval number ${count} is being cleared out`
      );
      clearInterval(myInterval);
    };
  }, [count]); // count in dependency array

  return (
    <div>
      {console.log('rendered or re-rendered')}
      <h1>UseEffectTest Component</h1>
      <button onClick={() => setToggleOne(!toggleOne)}>ToggleOne</button>
      <button onClick={() => setToggleTwo(!toggleTwo)}>ToggleTwo</button>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </div>
  );
};

export default UseEffectTest;