Skip to content

Stop Overusing useEffect: How to Simplify Your React Logic

A developer on a popular React forum recently asked: “Is it possible to remove the use of useEffect or are they unavoidable? My components are becoming hard to follow because of them.”

This is a common pain point. In earlier versions of React, we were taught to think in lifecycle methods. When Hooks arrived, many developers treated useEffect as a catch-all replacement for componentDidMount and componentDidUpdate. However, in modern React (especially version 18 and later), useEffect is considered an “escape hatch” rather than a primary tool.

The Immediate Fix: The “Effect-Less” Mindset

Section titled “The Immediate Fix: The “Effect-Less” Mindset”

Most useEffect hooks can be removed by following two rules:

  1. If you can calculate something during render, do it. (Derived State)
  2. If a side effect is triggered by a specific user action, move it to the event handler. (Event-driven logic)

Solution 1: Replace Effects with Derived State

Section titled “Solution 1: Replace Effects with Derived State”

Many developers use an effect to update one state variable based on another. This causes an unnecessary second render.

The Anti-pattern (Avoid this):

// React 18 - Illustrative example
function SearchComponent({ items }) {
const [filter, setFilter] = useState('');
const [filteredItems, setFilteredItems] = useState([]);
useEffect(() => {
// This triggers a second render every time filter changes
setFilteredItems(items.filter(item => item.includes(filter)));
}, [filter, items]);
return <input value={filter} onChange={e => setFilter(e.target.value)} />;
}

The Better Way (Derived State):

// React 18+
function SearchComponent({ items }) {
const [filter, setFilter] = useState('');
// Calculate during render. No useEffect needed!
const filteredItems = items.filter(item => item.includes(filter));
return <input value={filter} onChange={e => setFilter(e.target.value)} />;
}

Why this works: React calculates filteredItems as part of the normal render cycle. It is faster, uses less memory, and ensures the UI is always in sync with the state without waiting for a second “effect” pass.


Solution 2: Move Side Effects to Event Handlers

Section titled “Solution 2: Move Side Effects to Event Handlers”

If you are using useEffect to “watch” a state change so you can perform an action (like an API call or showing a notification), you should usually move that logic to the event that triggered the state change in the first place.

The Anti-pattern (Avoid this):

// React 18 - Illustrative example
function Form() {
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
postDataToServer();
showNotification("Success!");
}
}, [submitted]);
return <button onClick={() => setSubmitted(true)}>Submit</button>;
}

The Better Way (Event Handling):

// React 18+
function Form() {
function handleSubmit() {
// Handle the state change AND the side effect in the same place
setSubmitted(true);
postDataToServer();
showNotification("Success!");
}
return <button onClick={handleSubmit}>Submit</button>;
}

Why this works: useEffect is for synchronization (keeping your component in sync with an external system like a Chat Room or a Window API). A user clicking a button is an event, not a synchronization issue. Handling it in the event handler makes the code imperative and much easier to debug.


While you can remove 60-80% of effects in a typical app, useEffect is still necessary for External Systems. These include:

  • Connecting to a WebSocket server or a subscription.
  • Controlling a non-React widget (e.g., a Google Maps instance or a D3 chart).
  • Manually manipulating the DOM when there is no other way (e.g., adjusting scroll position or focus in complex scenarios).

In React 18, the introduction of “Strict Mode” (which mounts components twice in development) was specifically designed to catch bugs caused by improper useEffect usage. If your effect breaks when it runs twice, it’s a sign that your logic isn’t idempotent or that you are using an effect where an event handler should be.

By removing unnecessary effects, you avoid “State Glitches” (where the UI shows old data for a split second before the effect finishes) and reduce the complexity of the dependency array, which is the #1 source of infinite loops in React.


1. What about performance? Won’t recalculating data on every render be slow? In 95% of cases, no. Modern JavaScript engines are incredibly fast at filtering arrays or performing calculations. If you are doing something genuinely expensive (like processing 10,000+ items), you should wrap the calculation in useMemo instead of useEffect. This caches the result without triggering extra renders.

2. Should I stop using useEffect for data fetching (API calls)? Ideally, yes. For modern React apps, it is recommended to use libraries like TanStack Query (React Query) or SWR, or framework-level features (like Remix/Next.js loaders). These tools handle caching, race conditions, and loading states much better than a manual useEffect fetch, which often suffers from “Race Conditions” (where an older API request finishes after a newer one, overwriting the data).

3. Is there a “checklist” to know if I should delete an effect? Ask yourself:

  • “Am I just transforming data?” → Move to render/useMemo.
  • “Am I reacting to a user action (click/submit)?” → Move to event handler.
  • “Am I trying to reset state when a prop changes?” → Use a key prop on the component instead.
  • “Am I synchronizing with something outside React?” → Keep the useEffect.