useEffect Hook in React
useEffect
useEffect
allows you to perform side effects in functional components, such as fetching data, subscribing to events, or manipulating the DOM.
The basic syntax of useEffect is:
import { useEffect } from "react";
function MyComponent() {
useEffect(() => {
// code to run on mount or update
// Optional cleanup function
return () => {
// Code to clean up resources or side effects
};
}, [dependencies]);
return <div>My Component</div>;
}
The useEffect hook takes two arguments:
- Callback function: Contains the code for the side effect. Optionally returns a cleanup function.
- Dependency array (optional): Specifies when the side effect should run.
How useEffect Works
When you use useEffect
in a React component, it handles what happens after the component renders. Here's how it works in two scenarios:
- When the Component Mounts:
- React renders your component for the first time.
- The code inside the useEffect runs (this is called the "effect").
- When the Component Updates:
- React re-renders your component if its state or props change.
- The useEffect runs again if any value in the "dependency array" has changed.
The behavior of useEffect depends on the dependency array you provide. Let’s go through some examples:
-
Runs after every render: If you don’t provide a dependency array, the effect will run every time the component renders, whether it’s the first render (mount) or any update.
import React, { useEffect } from "react";
function App() {
useEffect(() => {
console.log("Component rendered or updated");
});
return <div>Hello, World!</div>;
} -
Runs only on mount: If you provide an empty dependency array
([])
, the effect will run only once, after the component is first rendered.import { useState, useEffect } from "react";
function MyComponent() {
const [data, setData] = useState([]);
useEffect(() => {
fetch("https://api.example.com/data")
.then((response) => response.json())
.then((data) => setData(data));
}, []); // empty dependencies array means run only on mount
return <div>Data: {data}</div>;
} -
Runs on Mount and When Dependencies Change:
If you provide specific values in the dependency array, the effect will run:
- When the component mounts.
- Whenever any value in the dependency array changes.
import { useEffect } from "react";
function MyComponent({ title }) {
useEffect(() => {
document.title = title;
}, [title]); // re-run effect when title changes
return <div>My Component</div>;
}
Asynchronous Effects
The useEffect
callback cannot be async
directly, but you can call an async
function inside it.
import React, { useState, useEffect } from "react";
function App() {
const [data, setData] = useState(null);
// Here the callback function is useEffect is not async but we call the async function inside it.
useEffect(() => {
const fetchData = async () => {
const response = await fetch("https://api.example.com/data");
const json = await response.json();
setData(json);
};
fetchData();
}, []); // Dependency array ensures this runs only once.
return <div>{data ? JSON.stringify(data) : "Loading..."}</div>;
}
Cleanup Functions In useEffect
A cleanup function in useEffect
is a way to clean up side effects before the effect is re-executed or when the component is unmounted.
Cleanup functions are especially useful for preventing memory leaks. Mentioned below are some examples of it.
-
Removing event listeners:
import React, { useEffect, useState } from "react";
function WindowResizeListener() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
// Add event listener
window.addEventListener("resize", handleResize);
// Cleanup function to remove the event listener
return () => {
window.removeEventListener("resize", handleResize);
};
}, []); // Empty dependency array means this effect runs only once
return <div>Window width: {width}px</div>;
}
export default WindowResizeListener; -
Clearing timers or intervals:
import React, { useEffect, useState } from "react";
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
// Cleanup function to clear the interval
return () => {
clearInterval(interval);
};
}, []); // Empty array ensures the effect runs only once
return <div>Timer: {count}s</div>;
}
export default Timer; -
Canceling subscriptions:
import React, { useEffect, useState } from "react";
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
let isMounted = true; // Track if the component is still mounted
const fetchData = async () => {
const response = await fetch("https://api.example.com/data");
const result = await response.json();
if (isMounted) {
setData(result); // Update state only if the component is mounted
}
};
fetchData();
// Cleanup function to mark the component as unmounted
return () => {
isMounted = false;
};
}, []); // Runs only on mount
return <div>{data ? JSON.stringify(data) : "Loading..."}</div>;
}
export default DataFetcher;
Avoiding Common Pitfalls
-
Infinite Loops: Avoid modifying state inside the effect without proper conditions.
useEffect(() => {
setCount((prev) => prev + 1); // Triggers a render loop.
}, []); // Fix: Add proper condition or dependency. -
Missing Dependency: Forgetting to include all dependencies in the array can cause stale closures or unexpected behavior.
useEffect(() => {
console.log(variable); // Make sure `variable` is in the dependency array.
}, []); // Add `variable` to avoid stale data.