Skip to content

What Are the Legitimate Use Cases for useEffect in React?

Problem: My useEffect Hook Is Triggering Unexpected Re-renders

I was debugging a React component last week when I realized I had fallen into a common trap. My code looked something like this:

UserProfile.jsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
useEffect(() => {
// Effect 1: Fetch user when userId changes
fetchUser(userId).then(setUser);
}, [userId]);
useEffect(() => {
// Effect 2: Fetch posts when user changes
if (user) {
fetchPosts(user.id).then(setPosts);
}
}, [user]);
// ... render logic
}

The cascade of effects was causing unnecessary re-renders and network requests. A fellow developer on Reddit confessed to the same pattern: “If prop A changed, I had an effect to update state B, which naturally triggered another effect to fetch some data.”

This made me ask: What are the legitimate use cases for useEffect, and when am I just overcomplicating my code?

Purpose: Understanding When useEffect Is Truly Necessary

After diving into the React documentation and community discussions, I discovered that useEffect has a very specific purpose: synchronizing with external systems. Most of my useEffect hooks were solving problems that didn’t require effects at all.

The Golden Rule: External System Synchronization

The React team defines legitimate useEffect use cases as operations that need to “step outside” React’s rendering cycle to interact with the external world:

  • Browser APIs (DOM measurements, event listeners)
  • Third-party libraries (charting, animation libraries)
  • Network requests (fetching data from servers)
  • Subscriptions (WebSocket connections, observables)
  • Timers (setTimeout, setInterval)
  • External stores (localStorage, IndexedDB)

If your code only transforms or computes values from React state/props, you don’t need useEffect.

Legitimate Use Case 1: Browser API Integration

Browser APIs like window.addEventListener or document manipulations require cleanup and are external to React’s lifecycle.

useWindowWidth.js
import { useState, useEffect } from 'react';
function useWindowWidth() {
const [width, setWidth] = useState(
typeof window !== 'undefined' ? window.innerWidth : 0
);
useEffect(() => {
// This is a legitimate effect: subscribing to browser API
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
// Cleanup is essential for browser event listeners
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Empty deps: run once on mount
return width;
}

This is legitimate because:

  • window.addEventListener is external to React
  • We need to clean up the subscription on unmount
  • The effect synchronizes React state with browser state

Legitimate Use Case 2: Third-Party Library Integration

When integrating non-React libraries, useEffect bridges the gap between React’s declarative model and imperative library APIs.

ChartComponent.jsx
import { useEffect, useRef } from 'react';
import Chart from 'chart.js';
function ChartComponent({ data, options }) {
const canvasRef = useRef(null);
const chartRef = useRef(null);
useEffect(() => {
// Create Chart.js instance
const ctx = canvasRef.current.getContext('2d');
chartRef.current = new Chart(ctx, {
type: 'line',
data: data,
options: options
});
// Cleanup Chart.js instance
return () => {
if (chartRef.current) {
chartRef.current.destroy();
}
};
}, [data, options]);
return <canvas ref={canvasRef} />;
}

Chart.js doesn’t know about React’s rendering cycle. We use useEffect to synchronize the Chart.js instance with React’s component lifecycle.

Legitimate Use Case 3: WebSocket Subscriptions

Real-time data subscriptions require connecting to external systems and cleaning up when the component unmounts.

useWebSocket.js
import { useEffect, useState } from 'react';
function useWebSocket(url) {
const [messages, setMessages] = useState([]);
const [connectionStatus, setConnectionStatus] = useState('connecting');
useEffect(() => {
const socket = new WebSocket(url);
socket.onopen = () => setConnectionStatus('connected');
socket.onmessage = (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};
socket.onerror = () => setConnectionStatus('error');
socket.onclose = () => setConnectionStatus('disconnected');
// Cleanup: close the WebSocket connection
return () => {
socket.close();
};
}, [url]);
return { messages, connectionStatus };
}

This effect is essential because:

  • WebSocket connections are external resources
  • They need explicit cleanup to prevent memory leaks
  • The connection lifecycle doesn’t align with React’s render cycle

Legitimate Use Case 4: External Store Synchronization

When reading from or writing to external stores like localStorage, useEffect ensures React state stays synchronized.

useLocalStorage.js
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Initialize state
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('Error reading localStorage:', error);
return initialValue;
}
});
// Synchronize to localStorage when value changes
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error('Error writing to localStorage:', error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}

Note: For more complex external store synchronization, consider using useSyncExternalStore introduced in React 18.

Common Anti-Patterns: When NOT to Use useEffect

After reviewing my code, I found several patterns where I was using useEffect unnecessarily.

Anti-Pattern 1: Derived State

BadDerivedState.jsx
// ❌ WRONG: Using effect for derived state
function UserList({ users }) {
const [sortedUsers, setSortedUsers] = useState([]);
useEffect(() => {
setSortedUsers([...users].sort((a, b) => a.name.localeCompare(b.name)));
}, [users]);
return <ul>{sortedUsers.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// ✅ CORRECT: Derive state during render
function UserList({ users }) {
const sortedUsers = [...users].sort((a, b) =>
a.name.localeCompare(b.name)
);
return <ul>{sortedUsers.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

Anti-Pattern 2: Handling Events

BadEventHandling.jsx
// ❌ WRONG: Using effect to respond to events
function SubmitButton({ onSubmit }) {
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
onSubmit();
setSubmitted(false);
}
}, [submitted, onSubmit]);
return (
<button onClick={() => setSubmitted(true)}>
Submit
</button>
);
}
// ✅ CORRECT: Handle events directly in event handler
function SubmitButton({ onSubmit }) {
const handleSubmit = () => {
onSubmit();
};
return (
<button onClick={handleSubmit}>
Submit
</button>
);
}

Anti-Pattern 3: Data Fetching Without Caching

While data fetching is a legitimate use case, raw useEffect can lead to race conditions and lacks caching.

NaiveDataFetching.jsx
// ⚠️ RISKY: Manual data fetching can cause issues
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
return user ? <div>{user.name}</div> : <div>Loading...</div>;
}

A community comment from Reddit highlights this: “It depends, some still need useEffect. You can extract to use some lib like tanstack Query or else or make your own hook.”

The better approach:

BetterDataFetching.jsx
// ✅ BETTER: Use a data fetching library
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data: user, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json())
});
return isLoading ? <div>Loading...</div> : <div>{user.name}</div>;
}

Key Insights from the Community

The Reddit discussion revealed several important perspectives:

On Architecture:

“A lot of these issues arise from having stuff that doesn’t belong in the UI layer. Shared data, business logic, network code etc. all belong outside React runtime.”

This suggests that the problem isn’t useEffect itself, but where we place our business logic.

On Data Fetching:

“This is why pure components are so important. Don’t fetch data at all, get it from the props.”

Moving data fetching to a parent component or a state management layer can eliminate many useEffect hooks.

On When Effects Are Needed:

“Sometimes they’re legit, let’s say setting initial state on page load based on some external variable (e.g. URL params) or syncing with an external store.”

External synchronization remains the core legitimate use case.

Summary: The useEffect Decision Tree

Before adding a useEffect hook, ask yourself:

┌─────────────────────────────────────────┐
│ Do I need to interact with an external │
│ system (browser API, network, etc.)? │
└─────────────┬───────────────────────────┘
┌────────┴────────┐
│ │
YES NO
│ │
▼ ▼
┌──────────┐ ┌──────────────────┐
│ useEffect │ │ Can I compute │
│ may be │ │ during render? │
│ needed │ └────────┬─────────┘
└──────────┘ │
┌──────┴──────┐
│ │
YES NO
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ Derive │ │ Is this in │
│ directly │ │ event handler?│
│ in render│ └──────┬───────┘
└──────────┘ │
┌──────┴──────┐
│ │
YES NO
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ Put in │ │ Reconsider │
│ onClick/ │ │ architecture │
│ onChange │ │ or use lib │
└──────────┘ └──────────────┘

Final Recommendations

  1. Use useEffect for: Browser APIs, third-party libraries, subscriptions, WebSocket connections, external store synchronization
  2. Avoid useEffect for: Derived state, event handling, simple state transformations
  3. Consider libraries for: Data fetching (TanStack Query), form state (React Hook Form), router state (React Router)
  4. Remember cleanup: Always return a cleanup function for subscriptions and event listeners

The key principle remains: useEffect synchronizes React with the external world. If you’re not leaving React’s ecosystem, you probably don’t need it.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments