1. Introduction
useEffect is one of the most commonly used hooks in React, enabling you to handle side effects—such as data fetching, subscribing to events, or updating the DOM—directly within functional components. While its core usage remains largely the same as previous React versions, React 19 continues to refine how developers approach side effects and concurrency, making it worth revisiting best practices for useEffect.
In this guide, we’ll cover:
- What useEffect does in React 19
- Common use cases and dependency management
- Advanced patterns for performance and concurrency
- Common pitfalls and how to avoid them
2. The Signature
useEffect(setup, dependencies?)
Where:
- setup (Required):
A function containing the side-effect logic you want to run after React renders (or re-renders) the component.- Optionally, this setup function can return a cleanup function. The returned cleanup runs before the next effect is invoked or when the component unmounts.
- dependencies? (Optional):
An array of values that determines when the effect fires.- No array: The effect runs after every render.
- Empty array ([]): The effect runs only once (on mount) and cleans up on unmount.
- Specified dependencies ([dep1, dep2, ...]): The effect runs whenever any listed dependency changes between renders.
3. Common Use Cases
3.1 Data Fetching
Fetching data from APIs after render is one of the most common patterns:
useEffect(() => {
fetch("/api/users")
.then((response) => response.json())
.then((data) => setUsers(data));
}, []); // Only fetch once on mount
3.2 Event Listeners
Attaching (and detaching) window or document event listeners:
useEffect(() => {
function handleResize() {
console.log("Window size:", window.innerWidth, window.innerHeight);
}
window.addEventListener("resize", handleResize);
// Cleanup when unmounted
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
3.3 Subscriptions
Subscribing to WebSockets, real-time data streams, or external services:
useEffect(() => {
const subscription = chatService.subscribe((message) => {
addMessage(message);
});
return () => {
subscription.unsubscribe();
};
}, [chatService]);
4. How Dependencies Work
Dependencies control when your effect re-runs:
- No Dependencies
- The effect runs after every render—useful for logic that needs to check or update on each update cycle.
- Empty Array ([])
- Mimics componentDidMount (and componentWillUnmount for cleanup). The effect fires only once when the component first mounts.
- One or More Dependencies
- The effect re-runs whenever any dependency changes. For example:
useEffect(() => {
// Update something whenever `count` or `userId` changes
}, [count, userId]);
- Be sure to list all variables your effect uses to avoid stale data or missing updates.
Tip: Enable the react-hooks/exhaustive-deps ESLint rule to catch missing dependencies in your projects.
5. Cleanup Function
A cleanup function can be returned from your setup:
useEffect(() => {
// Setup
const timer = setInterval(() => {
console.log("Interval is running!");
}, 1000);
// Cleanup
return () => clearInterval(timer);
}, []);
- React calls the cleanup function when the effect is about to re-run or the component is unmounting.
- This pattern prevents memory leaks and ensures you don’t leave listeners, timers, or subscriptions active.
6. Example: Fetching Data Once
Below is a more complete example showing how you might fetch data on mount, then store that data in component state:
import React, { useState, useEffect } from "react";
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
let isMounted = true;
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => res.json())
.then((data) => {
if (isMounted) {
setUsers(data);
}
})
.catch(console.error);
// Cleanup to avoid setting state if unmounted
return () => {
isMounted = false;
};
}, []); // Run once on mount
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export default UserList;
This approach ensures we only fetch data a single time when the component appears. We also safeguard the state update in case the component unmounts before the fetch resolves.
7. Advanced Patterns for React 19
7.1 Concurrency Considerations
With React 19 and concurrent rendering, useEffect can sometimes be delayed or interrupted if higher-priority tasks (like user input) come in. Generally, you don’t need to change your existing approach—React schedules these effects, maintaining a smooth user experience. However, be mindful that effects aren’t run during server-side rendering.
7.2 Conditional Effects
If you only want to run an effect when a condition is met:
useEffect(() => {
if (shouldFetch) {
fetchData();
}
}, [shouldFetch]);
Here, the effect triggers only when shouldFetch transitions to a truthy value.
7.3 Performance Optimization
Expensive operations within effects can cause performance bottlenecks. Consider:
- Memoization (useMemo or caching) if the effect repeatedly does the same work.
- Debounce or throttle (from a library like Lodash) if your effect is triggered too frequently (e.g., on every keystroke).
8. Common Pitfalls
- Infinite Loops
- Occur when you update state inside the effect without proper dependency control. If the updated state is listed as a dependency, the effect re-runs indefinitely.
- Missing Dependencies
- Failing to list all used variables in dependencies can lead to stale closures or inconsistent data.
- Leaking Resources
- Not cleaning up event listeners, intervals, or subscriptions leads to memory leaks.
- Overuse or Misuse
- If logic can be done outside of rendering cycles or is purely synchronous, you may not need useEffect.
9. Best Practices
- Keep Effects Focused: Each effect should handle one concern—like data fetching, logging, or subscriptions.
- Use Cleanup: Always return cleanup functions for timers or listeners to prevent leaks.
- Test Carefully: Especially under concurrency, test that your effects behave correctly on various devices and network conditions.
- Lint Your Hooks: The react-hooks/exhaustive-deps rule helps avoid errors in dependency arrays.
- Profile for Performance: If you see slow UIs, use the React Profiler to trace frequent re-renders or heavy operations in your effects.
10. Conclusion
The useEffect hook remains a cornerstone of React 19, enabling concise, powerful side-effect management in functional components. By understanding its signature—
useEffect(setup, dependencies?)
—and following best practices around dependencies, cleanup, and performance, you can build maintainable and efficient applications.
Key Takeaways
- One Hook, Many Lifecycles: Replace various class lifecycle methods with a single, flexible API.
- Careful Dependency Control: Correctly handle dependencies to avoid stale data or infinite loops.
- Cleanup & Performance: Leverage cleanup functions to manage resources and keep your app responsive.
- Concurrent-Friendly: useEffect seamlessly integrates with React’s concurrency features, helping you deliver a smoother user experience in React 19.
If you have questions, tips, or advanced use cases for useEffect, feel free to share in the comments below. Happy coding!
'Dev > React' 카테고리의 다른 글
A Practical Guide to useImperativeHandle in React 19 (0) | 2025.03.08 |
---|---|
A Practical Guide to useId in React 19 (0) | 2025.03.08 |
A Practical Guide to useDeferredValue in React 19 (0) | 2025.03.05 |
A Practical Guide to useDebugValue in React 19 (0) | 2025.03.05 |
A Practical Guide to useContext in React 19 (0) | 2025.03.05 |