Home / Articles / Stop defaulting to useEffect for every React problem

This article is published in English.

Stop defaulting to useEffect for every React problem

Effects sync with external systems; derive in render, handle clicks in events, fetch with real data tools.

1424 words

The useEffect trap

Hooks learners often reach for useEffect for fetching, derived state, and button clicks. Effects synchronize with external systems—they are not the default hammer for ordinary render logic.

What useEffect actually does

After paint, React runs effects to attach listeners, open connections, or imperatively talk to something outside React. Misuse creates flicker, double-fetch, and race bugs.

// ❌ BAD: Two renders, unnecessary state
function UserProfile({ firstName, lastName }) {
  const [fullName, setFullName] = useState('');
useEffect(() => {
    setFullName(`${firstName} ${lastName}`);
  }, [firstName, lastName]);
  return <h1>{fullName}</h1>;
}
// ✅ GOOD: Calculate during render
function UserProfile({ firstName, lastName }) {
  const fullName = `${firstName} ${lastName}`;
return <h1>{fullName}</h1>;
}
// ❌ BAD: Unnecessary effect + state
function ProductList({ products }) {
  const [sortedProducts, setSortedProducts] = useState([]);
useEffect(() => {
    const sorted = [...products].sort((a, b) => a.price - b.price);
    setSortedProducts(sorted);
  }, [products]);
  return (
    <ul>
      {sortedProducts.map(product => (
        <li key={product.id}>{product.name} - ${product.price}</li>
      ))}
    </ul>
  );
}
// ✅ GOOD: Calculate during render (useMemo for expensive ops)
function ProductList({ products }) {
  const sortedProducts = useMemo(() => {
    return [...products].sort((a, b) => a.price - b.price);
  }, [products]);
return (
    <ul>
      {sortedProducts.map(product => (
        <li key={product.id}>{product.name} - ${product.price}</li>
      ))}
    </ul>
  );
}
// ❌ BAD: Manual fetching with useEffect
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
useEffect(() => {
    let cancelled = false;
    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) throw new Error('Failed to fetch');
        const data = await response.json();
        if (!cancelled) setUser(data);
      } catch (err) {
        if (!cancelled) setError(err.message);
      } finally {
        if (!cancelled) setLoading(false);
      }
    }
    fetchUser();
    return () => { cancelled = true; };
  }, [userId]);
  if (loading) return <Spinner />;
  if (error) return <p>Error: {error}</p>;
  return <div>{user.name}</div>;
}
// ✅ GOOD: Use TanStack Query (React Query)
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
  const { data: user, isPending, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
  });
  if (isPending) return <Spinner />;
  if (error) return <p>Error loading user</p>;
  return <div>{user.name}</div>;
}
// ❌ BAD: Overcomplicating with useEffect
function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false);
useEffect(() => {
    if (liked) {
      fetch(`/api/posts/${postId}/like`, { method: 'POST' });
    } else {
      fetch(`/api/posts/${postId}/unlike`, { method: 'POST' });
    }
  }, [liked, postId]);
  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? '❤️' : '🤍'}
    </button>
  );
}
// ✅ GOOD: Handle in the event handler
function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false);
const handleLike = async () => {
    const newLiked = !liked;
    setLiked(newLiked);
    await fetch(`/api/posts/${postId}/${newLiked ? 'like' : 'unlike'}`, {
      method: 'POST',
    });
  };
  return (
    <button onClick={handleLike}>
      {liked ? '❤️' : '🤍'}
    </button>
  );
}
function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);

    // Cleanup is crucial!
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  return <p>Window width: {width}px</p>;
}
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
useEffect(() => {
    const connection = createWebSocketConnection(roomId);

    connection.onMessage = (msg) => {
      setMessages(prev => [...prev, msg]);
    };
    connection.connect();
    return () => {
      connection.disconnect(); // Cleanup!
    };
  }, [roomId]);
  return <MessageList messages={messages} />;
}
function ChartComponent({ data }) {
  const chartRef = useRef(null);
  const chartInstance = useRef(null);
useEffect(() => {
    if (chartRef.current && !chartInstance.current) {
      chartInstance.current = new ChartLibrary(chartRef.current, {
        type: 'line',
        data: data,
      });
    }
    return () => {
      chartInstance.current?.destroy();
      chartInstance.current = null;
    };
  }, []);
  return <canvas ref={chartRef} />;
}
function PageTitle({ title }) {
  useEffect(() => {
    document.title = `${title} | My App`;

    return () => {
      document.title = 'My App'; // Reset on unmount
    };
  }, [title]);
return null;
}

Four common mistakes

1. Calculating values in effects

Derive during render (or useMemo when expensive). Effects that setState from props/state often encode forbidden feedback loops.

2. Expensive calculations in effects

Keep pure computation in render/memo, not after paint.

3. Data fetching as an ungoverned effect

Fetching needs cancellation, caching, and error UX. Prefer dedicated libraries or frameworks with loading boundaries; if using effects, handle abort and races.

4. User actions in effects

Click handlers belong on events. Effects that fire because a flag flipped from a click add indirection and bugs.

Decision tree

If it calculates from existing state/props → render. If it responds to a click/submit → event handler. If it talks to a DOM API, network feed, or third-party widget lifecycle → effect.

When effects are right

External event listeners

WebSockets

Third-party library initialization

Imperative non-React APIs that must dispose cleanly

Closing

Fewer effects, clearer data flow. Reserve useEffect for synchronization with systems outside React—not as a default hammer for every post-render thought.

Strict Mode double-invokes effects in development to surface missing cleanups—treat that as a feature when wiring sockets. Prefer declarative data libraries for server state so components describe dependencies instead of hand-rolling fetch effects. Measure before adding effects that only copy props into state; that pattern is usually a misunderstanding of render.

Cleanup functions must tear down listeners and timers; lingering handlers are a classic source of setState-on-unmounted warnings.

Dependency arrays are part of the contract—omit values and stale closures appear; over-list objects and effects thrash every render.

When synchronizing with URL search params or external stores, prefer purpose-built hooks that encapsulate the effect boundary.

Server-rendered apps should not start browser-only effects without guards; gate on environment or lifecycle.

Cleanup functions must tear down listeners and timers; lingering handlers are a classic source of setState-on-unmounted warnings.

Dependency arrays are part of the contract—omit values and stale closures appear; over-list objects and effects thrash every render.

When synchronizing with URL search params or external stores, prefer purpose-built hooks that encapsulate the effect boundary.

Server-rendered apps should not start browser-only effects without guards; gate on environment or lifecycle.

Cleanup functions must tear down listeners and timers; lingering handlers are a classic source of setState-on-unmounted warnings.

Dependency arrays are part of the contract—omit values and stale closures appear; over-list objects and effects thrash every render.

When synchronizing with URL search params or external stores, prefer purpose-built hooks that encapsulate the effect boundary.

Server-rendered apps should not start browser-only effects without guards; gate on environment or lifecycle.

Cleanup functions must tear down listeners and timers; lingering handlers are a classic source of setState-on-unmounted warnings.

Dependency arrays are part of the contract—omit values and stale closures appear; over-list objects and effects thrash every render.

When synchronizing with URL search params or external stores, prefer purpose-built hooks that encapsulate the effect boundary.

Server-rendered apps should not start browser-only effects without guards; gate on environment or lifecycle.

Cleanup functions must tear down listeners and timers; lingering handlers are a classic source of setState-on-unmounted warnings.

Dependency arrays are part of the contract—omit values and stale closures appear; over-list objects and effects thrash every render.

When synchronizing with URL search params or external stores, prefer purpose-built hooks that encapsulate the effect boundary.

Server-rendered apps should not start browser-only effects without guards; gate on environment or lifecycle.

Cleanup functions must tear down listeners and timers; lingering handlers are a classic source of setState-on-unmounted warnings.

Dependency arrays are part of the contract—omit values and stale closures appear; over-list objects and effects thrash every render.

When synchronizing with URL search params or external stores, prefer purpose-built hooks that encapsulate the effect boundary.

Server-rendered apps should not start browser-only effects without guards; gate on environment or lifecycle.

Cleanup functions must tear down listeners and timers; lingering handlers are a classic source of setState-on-unmounted warnings.

Dependency arrays are part of the contract—omit values and stale closures appear; over-list objects and effects thrash every render.

When synchronizing with URL search params or external stores, prefer purpose-built hooks that encapsulate the effect boundary.

Server-rendered apps should not start browser-only effects without guards; gate on environment or lifecycle.

Cleanup functions must tear down listeners and timers; lingering handlers are a classic source of setState-on-unmounted warnings.