If you’re working with React, useEffect is probably a familiar face. It’s a powerful hook for managing side effects, but it’s not a catch-all solution. The React docs describe it as an escape hatch for syncing your component with the outside world, not a tool to throw at every post render task. Misuse it, and you’re in for a world of bugs and performance issues.
💡 What Is useEffect Really For?
useEffect is your connection to the JavaScript world beyond React’s render cycle. It’s designed for side effects that can’t be handled during rendering, such as:
- Fetching data from an API
- Subscribing to events (e.g., window resize)
- Manually updating the DOM
- Setting timers or intervals
- Cleaning up resources when a component unmounts
If your component needs to interact with something external to React, useEffect is the go-to tool.
Example: Subscribing to a window event
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
This code listens for window resize events and ensures proper cleanup when the component unmounts. classic useEffect territory.
🚫 When Not to Use useEffect
One of the biggest mistakes developers make is using useEffect for tasks that belong elsewhere. Here are common misuses and better alternatives.
Calculating Derived State
Don’t: Use useEffect to update state based on other state or props. This adds unnecessary complexity and can trigger extra re-renders.
// Bad
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
Do: Compute derived state directly in the render phase for simplicity and performance.
// Good
const fullName = `${firstName} ${lastName}`;
Filtering or Transforming Arrays
Don’t: Store filtered data in state via useEffect. This can slow down your app, especially with large datasets.
// Bad
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(todos.filter(t => !t.completed));
}, [todos]);
Do: Filter data directly in render to keep things efficient.
// Good
const visibleTodos = todos.filter(t => !t.completed);
Handling User Events
Don’t: Put user-triggered logic in useEffect. It makes your code harder to follow and debug.
// Bad
useEffect(() => {
if (formSubmitted) {
sendAnalytics();
}
}, [formSubmitted]);
Do: Handle user events in event handlers for clarity.
// Good
function handleSubmit() {
sendAnalytics();
// other logic...
}
Resetting State on Prop Change
Don’t: Use useEffect to reset state when props change. This can lead to unexpected behavior in concurrent rendering.
// Bad
useEffect(() => {
setComment('');
}, [userId]);
Do: Use a key prop to reset component state by forcing a remount.
// Good
<Profile key={userId} userId={userId} />
🚀 Level Up
Here are some tips to keep your code robust and efficient.
Master the Dependency Array
The dependency array controls when your effect runs. Always include every value from outside the effect that’s used inside it. Skipping dependencies can hide bugs. For functions or objects passed as dependencies, use useCallback or useMemo to prevent unnecessary re-runs.
Avoid Infinite Loops and Stale Closures
Infinite loops occur when an effect updates state that triggers itself again. Check your dependencies to avoid this. Stale closures (where an effect uses outdated values) can be tricky with async operations. Use refs to access the latest values when needed.
Cleanup Is Essential
Always return a cleanup function for subscriptions, timers, or other external resources to prevent memory leaks. In React Strict Mode, effects run twice in development, so ensure your cleanup is idempotent (safe to run multiple times).
Example: Cleaning up a fetch request
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal });
return () => controller.abort();
}, []);
Extract Custom Hooks for Reuse
If you’re repeating effect logic, extract it into a custom hook for cleaner, testable code.
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
function updateStatus() {
setIsOnline(navigator.onLine);
}
window.addEventListener('online', updateStatus);
window.addEventListener('offline', updateStatus);
return () => {
window.removeEventListener('online', updateStatus);
window.removeEventListener('offline', updateStatus);
};
}, []);
return isOnline;
}
useEffect vs useLayoutEffect
useEffect runs after the browser paints, ideal for non-visual tasks like data fetching. useLayoutEffect runs synchronously before painting, perfect for DOM measurements or visual tweaks that must happen before the user sees the screen.
Handle Race Conditions in Data Fetching
When fetching data, race conditions can occur if multiple requests are sent before the first completes. Use a flag or abort signal to ignore stale responses.
useEffect(() => {
let ignore = false;
fetchData().then(result => {
if (!ignore) setData(result);
});
return () => { ignore = true; };
}, [someDependency]);
Server vs Client Boundaries
useEffect only runs in the browser, not during server-side rendering. For logic needed on both server and client, handle it outside effects or in your data fetching layer.
🧠 Better Data Fetching: TanStack Query
While useEffect is versatile, specialized libraries can handle certain side effects more effectively, reducing boilerplate and potential errors.
For data fetching, TanStack Query (formerly React Query) simplifies server state management with features like automatic caching, background updates, and built in error handling, making it a cleaner alternative to raw useEffect for API calls.
import { useQuery } from '@tanstack/react-query';
function MyComponent() {
const { data, isLoading, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(res => res.json()),
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
TanStack Query handles caching and refetching out of the box, which would require significant manual effort with useEffect. Learn more at TanStack Query Docs.
🧹 Final Thoughts
useEffect is a powerful tool, but it’s not a hammer for every nail. Use it for syncing with external systems, and keep derived state, event handling, and data transformations in the render phase. Master dependency arrays, prioritize cleanup, and extract reusable logic into custom hooks.
For specific tasks like data fetching, libraries like TanStack Query can save you time and reduce bugs. By choosing the right tool for the job, you’ll build React apps that are fast, reliable, and a pleasure to maintain. Try these patterns in your next project and see how they streamline your workflow!
I highly recommend reading the React team’s amazing article, You Might Not Need an Effect, to better understand detailed use cases and common pitfalls.
Top comments (0)