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:
- No dependency array
- Empty dependency array
- Dependency array with state variables
- Conditional execution inside useEffect
- Cleanup function
Devising a Plan
- Create a
UseEffectTestcomponent - Add a
useEffectwith no dependency array - Add state and a button to trigger re-renders
- Modify the
useEffectto use an empty dependency array - Add a second state variable and
useEffectwith that variable in the dependency array - Add conditional logic inside the second
useEffect - Add a counter and a third
useEffectwith a cleanup function - 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?
- No dependency array: Runs after every render (initial render and all re-renders)
- Empty dependency array []: Runs only once, after the initial render
- With dependencies [dep1, dep2]: Runs after initial render and when any dependency changes
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:
- Runs before the next effect execution
- Also runs when the component unmounts
- Helps prevent memory leaks by cleaning up resources (timers, subscriptions, etc.)
Real World Examples
Some common use cases for useEffect:
- Fetching data from an API when a component mounts (empty dependency array)
- Subscribing to events or WebSockets (with cleanup to unsubscribe)
- Updating document title when relevant state changes
- Triggering animations when certain props change
- Syncing with localStorage when state updates
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;