跪拜 Guibai
← Back to the summary

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?

  1. One extra render: cart changes → renders once → useEffect triggers setTotal → renders again. Two renders do the work of one.
  2. One extra state: total is a derived value of cart, but giving it an independent lifecycle creates moments where cart and total can be out of sync.
  3. 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?

  1. Broken causality: When reading the code, you see setSubmitted(true) and have no idea what it triggers. Logic is scattered across two places.
  2. Uncontrollable execution timing: useEffect runs after rendering, not when the event occurs. There's a render cycle in between.
  3. Leaked temporary state: The submitted state 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:


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.