Published on 10.5.2020
Managing side effects is an essential part of React development. Whether it’s fetching data, subscribing to external events, or interacting with browser APIs, side effects can introduce complexity in your components. In this post, we’ll discuss how useEffect
has transformed how we manage side effects in React, highlight some common mistakes developers make, and explore best practices for ensuring clean, reusable side-effect management in your React components.
How useEffect
Changed the Way We Handle Side Effects in React
In earlier versions of React, handling side effects was done in lifecycle methods like componentDidMount
, componentDidUpdate
, and componentWillUnmount
in class components. However, the introduction of Hooks in React 16.8 brought a major shift, with useEffect
becoming the go-to solution for handling side effects in functional components.
useEffect
is called during the render phase, allowing you to perform side effects after the DOM has been updated. It can be thought of as a replacement for the lifecycle methods in class components, and it also enables more flexible and declarative ways of managing side effects.
How useEffect
Works
- Runs after rendering: By default,
useEffect
runs after the DOM is painted, which means it doesn’t block the UI and lets the browser update the screen first. - Dependency array: The second argument of
useEffect
—the dependency array—lets you specify when the effect should run. If you pass an empty array ([]
), the effect runs only once, after the initial render. If you pass specific values (likestate
orprops
), the effect runs when any of those dependencies change. - Cleanup function:
useEffect
can return a cleanup function to clean up resources when the component unmounts or the effect dependencies change.
Common Mistakes When Using useEffect
While useEffect
is powerful, it’s easy to make mistakes that can lead to bugs or performance issues. Here are a few common pitfalls to avoid:
1. Forgetting to Include Dependencies in the Dependency Array
One of the most common mistakes is forgetting to include all relevant variables in the dependency array. When dependencies change, the effect should re-run, but if you don’t list all dependencies, you may end up with outdated values or unexpected behavior.
Incorrect Example:
useEffect(() => {
// fetching data
fetchData();
}, []); // Missing dependency
In this case, if fetchData
relies on a state variable that changes, but isn’t included in the dependency array, the effect won’t run when the state changes.
Best Practice:
Always ensure that you list all values that are used inside the effect and come from outside of it (e.g., props or state).
useEffect(() => {
fetchData();
}, [stateVariable]); // Correct: Added necessary dependency
2. Creating Infinite Loops
If you accidentally change state inside the useEffect
without specifying the right dependency array, you may unintentionally trigger infinite loops. Every time the state changes, the effect will run, causing another state change, and so on.
Incorrect Example:
useEffect(() => {
setState(value); // Updating state inside useEffect
}, [state]); // This creates an infinite loop
Best Practice:
Be careful when updating state inside useEffect
. Ensure that your state changes are dependent on the right conditions, and try to avoid updating the state inside the effect unless necessary.
3. Not Cleaning Up Side Effects
When handling side effects such as subscriptions, timers, or network requests, it’s important to clean up those effects when the component unmounts or when the dependencies change. Failing to clean up can result in memory leaks and other issues.
Incorrect Example (no cleanup):
useEffect(() => {
const timer = setInterval(() => {
console.log('Interval running...');
}, 1000);
}, []); // No cleanup function
In this example, the interval will keep running indefinitely, even if the component unmounts, causing memory leaks.
Best Practice:
Always use the cleanup function to clean up resources when necessary.
useEffect(() => {
const timer = setInterval(() => {
console.log('Interval running...');
}, 1000);
// Cleanup
return () => clearInterval(timer);
}, []); // Cleanup on unmount
4. Using Effects for Non-Side-Effect Operations
useEffect
should be used for side effects, not for operations that can be done synchronously during the render. For example, don’t use it for calculations or other operations that don’t involve interacting with external systems.
Incorrect Example (non-side-effect operation):
useEffect(() => {
const result = expensiveCalculation(state); // This could be synchronous
}, [state]);
Best Practice:
For calculations or other pure operations, just run them directly inside the component’s render logic.
const result = expensiveCalculation(state); // Directly inside the component render logic
Best Practices for Managing Side Effects with Custom Hooks
To improve the reusability and readability of side-effect logic, it’s a great idea to extract side-effect logic into custom hooks. Custom hooks allow you to encapsulate side-effect code and reuse it across multiple components.
Example: Fetching Data with useEffect
in a Custom Hook
Let’s say you need to fetch data from an API in multiple components. Instead of repeating the same useEffect
logic in each component, you can create a custom hook.
import { useState, useEffect } from 'react';
function useFetchData(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]); // Dependency on URL to refetch when it changes
return { data, loading, error };
}
Now, you can use useFetchData
in any component where you need to fetch data:
function MyComponent() {
const { data, loading, error } = useFetchData('https://api.example.com/data');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <div>{JSON.stringify(data)}</div>;
}
By creating a custom hook like useFetchData
, you keep your component logic clean and reuse the data-fetching logic in multiple places without duplicating code.
Best Practices for Custom Hooks
- Encapsulate Side Effects: Place side-effect logic that’s used across multiple components inside custom hooks.
- Use State and Effects Appropriately: Custom hooks should encapsulate state and effects but allow components to manage how they handle the side effects (e.g., by passing parameters to the hook).
- Keep Hooks Simple and Focused: Each custom hook should handle a single concern (e.g., fetching data, subscribing to a WebSocket, etc.).
Conclusion
Managing side effects in React has never been easier thanks to useEffect
and custom hooks. By following best practices like properly handling dependencies, cleaning up side effects, and creating reusable hooks, you can ensure that your React applications remain efficient, maintainable, and bug-free.
Key Takeaways:
- Use
useEffect
for side effects such as data fetching, subscriptions, or timers. - Avoid common mistakes like forgetting dependencies, creating infinite loops, or neglecting cleanup.
- Use custom hooks to encapsulate side-effect logic and keep your components clean and reusable.
With the right practices, you can easily manage side effects and improve the overall structure of your React applications.