With the introduction of React Hooks in React 16.8, functional components gained the ability to manage side effects using the useEffect
Hook. Previously, handling side effects such as data fetching, subscriptions, and manual DOM manipulations was only possible within class components using lifecycle methods.
The useEffect
Hook simplifies this process, allowing developers to handle side effects directly inside functional components. In this post, we’ll explore how useEffect
works, how it replaces lifecycle methods like componentDidMount
, componentDidUpdate
, and componentWillUnmount
, and some practical use cases.
How useEffect
Works
useEffect
is a Hook that runs side effects in function components. Side effects include things like:
- Fetching data from an API
- Manually modifying the DOM
- Setting up subscriptions or event listeners
- Running timers or intervals
The basic syntax of useEffect
looks like this:
import React, { useEffect } from "react";
function ExampleComponent() {
useEffect(() => {
console.log("Effect runs after render");
return () => {
console.log("Cleanup function runs before unmounting");
};
}, []);
return <div>Check the console for logs</div>;
}
The first argument to useEffect
is a function that runs after the component renders. The optional second argument is an array of dependencies, which determines when the effect should re-run.
Replacing Class Lifecycle Methods with useEffect
In class components, lifecycle methods are used to manage side effects. Here’s how useEffect
replaces them.
1. Replacing componentDidMount
(Run Effect After First Render)
In class components, componentDidMount
is used for tasks like fetching data when a component is first displayed:
class ExampleComponent extends React.Component {
componentDidMount() {
console.log("Component mounted");
}
render() {
return <div>Class Component</div>;
}
}
With useEffect
, the same behavior is achieved by providing an empty dependency array ([]
), ensuring the effect runs only once after the initial render:
import React, { useEffect } from "react";
function ExampleComponent() {
useEffect(() => {
console.log("Component mounted");
}, []);
return <div>Function Component</div>;
}
2. Replacing componentDidUpdate
(Run Effect When Props or State Change)
In class components, componentDidUpdate
runs after a component updates, often used to respond to changes in props or state:
class ExampleComponent extends React.Component {
componentDidUpdate(prevProps) {
if (prevProps.value !== this.props.value) {
console.log("Component updated");
}
}
render() {
return <div>Value: {this.props.value}</div>;
}
}
With useEffect
, we pass the specific dependency that should trigger the effect when it changes:
import React, { useEffect } from "react";
function ExampleComponent({ value }) {
useEffect(() => {
console.log("Component updated");
}, [value]);
return <div>Value: {value}</div>;
}
If value
changes, the effect runs again.
3. Replacing componentWillUnmount
(Cleanup Before Component Unmounts)
In class components, componentWillUnmount
is used to clean up side effects like event listeners or timers:
class ExampleComponent extends React.Component {
componentDidMount() {
window.addEventListener("resize", this.handleResize);
}
componentWillUnmount() {
window.removeEventListener("resize", this.handleResize);
}
handleResize = () => {
console.log("Window resized");
};
render() {
return <div>Resize the window</div>;
}
}
With useEffect
, return a function inside the effect to handle cleanup:
import React, { useEffect } from "react";
function ExampleComponent() {
useEffect(() => {
const handleResize = () => {
console.log("Window resized");
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return <div>Resize the window</div>;
}
The cleanup function ensures that event listeners are removed when the component is unmounted.
Practical Use Cases of useEffect
1. Fetching Data from an API
Fetching data is a common side effect in React applications. Here’s how useEffect
handles an API request:
import React, { useEffect, useState } from "react";
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((response) => response.json())
.then((json) => setData(json));
}, []);
return <div>{data ? data.title : "Loading..."}</div>;
}
Since we provide an empty dependency array ([]
), the API call runs only once when the component mounts.
2. Event Listeners
Event listeners often require cleanup to avoid memory leaks. Here’s an example using a keydown listener:
import React, { useEffect } from "react";
function KeyLogger() {
useEffect(() => {
const handleKeyPress = (event) => {
console.log(`Key pressed: ${event.key}`);
};
window.addEventListener("keydown", handleKeyPress);
return () => {
window.removeEventListener("keydown", handleKeyPress);
};
}, []);
return <div>Press any key and check the console</div>;
}
The cleanup function ensures the event listener is removed when the component unmounts.
3. Setting and Cleaning Up Intervals
If your component needs to run an interval, you should always clear it when the component unmounts to prevent unwanted behavior:
import React, { useEffect, useState } from "react";
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return <div>Timer: {seconds} seconds</div>;
}
Conclusion
The useEffect
Hook provides a powerful and flexible way to manage side effects in functional components, replacing the need for lifecycle methods in class components.
Summary of Lifecycle Replacements
componentDidMount
→ UseuseEffect
with an empty dependency array[]
.componentDidUpdate
→ UseuseEffect
with specific dependencies.componentWillUnmount
→ UseuseEffect
with a cleanup function.
By understanding and applying useEffect
correctly, you can simplify your React components while maintaining clean and efficient code.
If you’re new to Hooks, you may also want to explore:
- How
useState
simplifies state management - Using
useReducer
for complex state logic - Creating custom Hooks for reusable functionality
Hooks are changing the way we write React applications, and this is just the beginning.