Most of Your useEffects Shouldn't Exist
I recently checked the useEffect hooks in my project and counted 37 of them. Then I reviewed each one and found that 29 didn't need useEffect at all. After deleting them, the components rendered faster, the code got shorter, and there were fewer bugs.
You Probably Write It Like This
Let's look at a piece of "standard" React code — you might write something similar every day:
const [items, setItems] = useState([]);
const [filtered, setFiltered] = useState([]);
const [keyword, setKeyword] = useState('');
useEffect(() => {
setFiltered(items.filter(item => item.name.includes(keyword)));
}, [items, keyword]);
Looks reasonable, right? When keyword changes, filter the list again.
But this useEffect is completely unnecessary.
const [items, setItems] = useState([]);
const [keyword, setKeyword] = useState('');
const filtered = items.filter(item => item.name.includes(keyword));
Three lines become one. No useEffect, no extra state, no dependency array, no redundant renders.
This is not an isolated case. Most useEffects are using side effects to solve problems that should be calculated directly during rendering.
Anti-Pattern 1: Using useEffect to Sync Derived State
This is the most common abuse — splitting a directly computable value into state + useEffect.
// ❌ Syncing with useEffect
const [cart, setCart] = useState([]);
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(cart.reduce((sum, item) => sum + item.price * item.qty, 0));
}, [cart]);
// ✅ Compute directly
const [cart, setCart] = useState([]);
const total = cart.reduce((sum, item) => sum + item.price * item.qty, 0);
Why is the first approach harmful?
- One extra render: cart changes → renders once → useEffect triggers setTotal → renders again. Two renders do the work of one.
- One extra state:
totalis a derived value ofcart, but giving it an independent lifecycle creates moments wherecartandtotalcan be out of sync. - One extra dependency array: one more thing to maintain, one more place to make a mistake.
Judgment criterion: Can this value be computed directly from existing state/props? If it can be computed, don't store it.
Anti-Pattern 2: Using useEffect to Respond to Events
Another high-frequency mistake — using useEffect to watch state changes to execute "event logic."
// ❌ useEffect pretending to handle events
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
sendAnalytics('form_submit');
showToast('提交成功');
setSubmitted(false);
}
}, [submitted]);
const handleSubmit = () => {
saveForm(data);
setSubmitted(true);
};
// ✅ Do it directly in the event handler
const handleSubmit = () => {
saveForm(data);
sendAnalytics('form_submit');
showToast('提交成功');
};
What's wrong with using useEffect to handle events?
- Broken causality: When reading the code, you see
setSubmitted(true)and have no idea what it triggers. Logic is scattered across two places. - Uncontrollable execution timing: useEffect runs after rendering, not when the event occurs. There's a render cycle in between.
- Leaked temporary state: The
submittedstate exists only to "pass a signal" and has no UI meaning.
Judgment criterion: Does this logic execute because "an event happened"? If yes, put it in the event handler; don't detour through useEffect.
Anti-Pattern 3: Fetching Data with useEffect (Without the Right Tool)
This is the most classic one:
// ❌ Raw useEffect for data fetching
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetchUser(userId)
.then(data => {
if (!cancelled) {
setUser(data);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => { cancelled = true; };
}, [userId]);
A simple data fetch, 20 lines of code. And it still doesn't handle caching, retries, deduplication, race conditions...
// ✅ Use React Query
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
Three lines, with built-in caching, retries, deduplication, window focus refetching, and race condition handling.
This doesn't mean "never fetch in useEffect." It means if your project has more than three data-fetching scenarios, writing raw useEffect is just reinventing a worse React Query.
| Feature | Raw useEffect | React Query / SWR |
|---|---|---|
| Caching | Write it yourself | Built-in |
| Deduplication | Write it yourself | Built-in |
| Race condition handling | Easy to forget | Built-in |
| Retries | Write it yourself | Built-in |
| loading/error | 3 useStates | Returned directly |
| Lines of code | 20+ | 3-5 |
Anti-Pattern 4: Using useEffect for One-Time Initialization Logic
// ❌ useEffect with empty deps "run once"
useEffect(() => {
initSDK({ appId: 'xxx' });
registerGlobalHandler();
}, []);
Looks harmless, but React Strict Mode executes it twice — the SDK initializes twice, the global handler registers twice.
// ✅ Module-level initialization
let initialized = false;
function App() {
if (!initialized) {
initSDK({ appId: 'xxx' });
registerGlobalHandler();
initialized = true;
}
return <div>...</div>;
}
Or a more elegant way:
// ✅ Use useRef to guarantee single execution
const didInit = useRef(false);
useEffect(() => {
if (didInit.current) return;
didInit.current = true;
initSDK({ appId: 'xxx' });
registerGlobalHandler();
}, []);
Judgment criterion: Is this logic related to the component's lifecycle? If it's just "execute once when the app starts," it doesn't belong to any component; don't put it inside one.
Anti-Pattern 5: Using useEffect to "Sync" Two States
// ❌ Using useEffect to keep two states in sync
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// ✅ Compute the derived value directly
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`;
This is essentially the same as Anti-Pattern 1, but it appears so frequently that it deserves its own mention.
If you think "when A changes, B should also change," then B is probably not state; it's a computation result of A.
So When Should You Use useEffect?
After listing five "shouldn't use" scenarios, let's talk about the truly correct use case — syncing with external systems.
// ✅ Correct usage 1: Manipulating the DOM
useEffect(() => {
const el = ref.current;
const observer = new ResizeObserver(entries => {
setWidth(entries[0].contentRect.width);
});
observer.observe(el);
return () => observer.disconnect();
}, []);
// ✅ Correct usage 2: WebSocket connection
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = (e) => setMessages(prev => [...prev, JSON.parse(e.data)]);
return () => ws.close();
}, [url]);
// ✅ Correct usage 3: Third-party library integration
useEffect(() => {
const chart = new Chart(canvasRef.current, config);
return () => chart.destroy();
}, [config]);
useEffect has only one correct purpose: keeping React components in sync with things outside React. DOM APIs, WebSockets, third-party libraries, browser APIs — these need useEffect.
If your useEffect only calls React's own APIs (setState, other hooks), you're probably using it wrong.
Ultimate Quick Reference
| What you want to do | Use useEffect? | What you should do |
|---|---|---|
| Compute a value from state/props | ❌ | Compute directly during render, or useMemo |
| Execute logic when a user clicks a button | ❌ | Write it in the event handler |
| Request a backend API | ❌ | React Query / SWR / framework loader |
| Send analytics after form submission | ❌ | Write it in onSubmit |
| Reset component state when props change | ❌ | Add a key to the component |
| Keep two states in sync | ❌ | Merge into one state, or compute a derived value |
| Initialize an SDK on app startup | ⚠️ | Module-level code or useRef guard |
| Listen to window resize | ✅ | useEffect + cleanup |
| Connect to a WebSocket | ✅ | useEffect + cleanup |
| Integrate D3/Chart or other third-party libraries | ✅ | useEffect + cleanup |
| Sync document.title | ✅ | useEffect |
One-sentence summary: useEffect is not a tool for "executing code when state changes"; it's a tool for "syncing with the outside world."
Self-Check Checklist
Go back to your project and mark each useEffect:
- Does it contain setState? → Probably can be deleted
- Is it responding to an event? → Move to the event handler
- Is it fetching data? → Switch to React Query
- Does the dependency array always have bugs? → Maybe you shouldn't be using useEffect at all
- If you delete this useEffect, can the component still work normally? → If yes, don't hesitate
Delete those unnecessary useEffects, and your components will become shorter, faster, and have fewer bugs. This isn't optimization — it's correction.
How many useEffects are in your project? How many can you delete after checking? Let's discuss in the comments.