React

useEffect Best Practices: When You Don't Need It At All

Learn useEffect best practices in React 19 and discover how derived state, event handlers, and the use() API replace most effects you reach for by habit.

By Laxaar Engineering Team Jul 9, 2026 9 min read
useEffect Best Practices: When You Don't Need It At All

Most useEffect calls in production React codebases don't need to exist. The React team has said as much in their documentation for years, yet the pattern persists. A component fetches data on mount with an effect, derives a value inside an effect, resets state in an effect responding to a prop change, and suddenly you're debugging stale closures at 11pm wondering why your dependency array made things worse.

The root problem is habit. useEffect feels like the obvious escape hatch whenever something needs to "happen" in a component. React 19 gives us better tools for almost every one of those cases: the use() hook, first-class Actions, and cleaner patterns for derived state and event-driven logic. This post maps each common misuse to its correct replacement.

We've refactored dozens of codebases at Laxaar and the pattern is consistent: every time we audit a React app, 60-70% of effects are either unnecessary or actively harmful. The good news is that fixing them makes the code shorter, not longer.

What you'll learn

Why effects run twice and what that actually means

useEffect is a synchronization mechanism. That's the mental model the React docs push, and it's the right one. An effect is React's way of saying: "keep this external system in sync with this component's state." The external system could be a DOM API, a WebSocket, a third-party widget, or a browser API like setInterval.

React 18 introduced Strict Mode double-invocation: effects mount, unmount, and mount again in development. This catches a real class of bugs: effects that don't clean up properly, subscriptions that leak, or code that assumes a single mount. If your effect breaks on the second mount, it was already broken; React just exposed it sooner.

The critical insight: if React remounting breaks your logic, you're probably not synchronizing with an external system. You're executing a one-time side effect in the wrong place. That's the signal to reach for an event handler instead.

Derived state: the most common effect anti-pattern

Derived state is a value computed from existing state or props. It's one of the most frequent triggers for unnecessary effects.

// Anti-pattern: effect to compute derived state
function ProductCard({ price, quantity }) {
  const [total, setTotal] = useState(0);

  useEffect(() => {
    setTotal(price * quantity);
  }, [price, quantity]);

  return <div>{total}</div>;
}

This creates a render cycle: props change, effect runs, setTotal triggers another render. It's two renders where one would do.

// Correct: derive during render
function ProductCard({ price, quantity }) {
  const total = price * quantity;
  return <div>{total}</div>;
}

If the computation is expensive, useMemo is the right tool. Not an effect.

function FilteredList({ items, query }) {
  const filtered = useMemo(
    () => items.filter(item => item.name.includes(query)),
    [items, query]
  );
  return <ul>{filtered.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}

The rule: if a value can be computed from props or state synchronously, compute it during render. An effect that only calls a setter with a derived value is always removable.

Event handlers vs effects: where the line is

This distinction trips up experienced developers. The question to ask is: "does this happen because the user did something, or because the component is in a certain state?"

User actions (clicks, form submits, keyboard events) belong in event handlers. State synchronization belongs in effects.

// Anti-pattern: effect responding to a user action flag
function CheckoutForm() {
  const [submitted, setSubmitted] = useState(false);

  useEffect(() => {
    if (submitted) {
      sendAnalyticsEvent('checkout_completed');
      setSubmitted(false);
    }
  }, [submitted]);

  return (
    <button onClick={() => setSubmitted(true)}>
      Complete Order
    </button>
  );
}

The sendAnalyticsEvent call fires in response to the user clicking the button. That's an event handler concern, not a synchronization concern.

// Correct: fire the event directly in the handler
function CheckoutForm() {
  function handleSubmit() {
    sendAnalyticsEvent('checkout_completed');
    // proceed with submission logic
  }

  return <button onClick={handleSubmit}>Complete Order</button>;
}

The test: if the effect's dependency is a boolean flag that flips from false to true and then has to reset, you're using an effect to simulate an event. Move it to the handler.

Fetching data without useEffect in React 19

Data fetching has historically been the biggest driver of useEffect usage. React 19's use() hook changes the pattern entirely for many cases.

use() is a hook that reads the value of a Promise or Context. You call it with a promise, React suspends rendering while the promise resolves, and you get the resolved value back — no effects needed.

import { use, Suspense } from 'react';

// The fetch is created outside the component (or passed as a prop)
function UserProfile({ userPromise }) {
  const user = use(userPromise);
  return <h1>{user.name}</h1>;
}

function App() {
  const userPromise = fetch('/api/user/1').then(res => res.json());

  return (
    <Suspense fallback={<p>Loading...</p>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

The promise is created at the point where you have the data dependencies (often a Server Component or a router loader). The component that consumes it stays clean.

For mutation-heavy workflows, React 19 Actions let form submissions and button handlers work with async operations and pending states without any effect:

function CreatePostForm() {
  async function createPost(formData) {
    'use server';
    await db.posts.create({ title: formData.get('title') });
  }

  return (
    <form action={createPost}>
      <input name="title" />
      <button type="submit">Publish</button>
    </form>
  );
}

The trade-off here is real: use() works well for data that's ready when the component renders, but you still need effects (or a data library like TanStack Query) for imperative fetches triggered by user interactions after mount.

Resetting state on prop change the right way

A common pattern is resetting a component's internal state when one of its props changes. Teams reach for an effect:

// Anti-pattern: effect to reset state on prop change
function CommentThread({ postId }) {
  const [comments, setComments] = useState([]);

  useEffect(() => {
    setComments([]);
  }, [postId]);

  // ...
}

This renders twice: once with stale comments, then again after the effect fires and clears them. React has a built-in solution: the key prop.

// Correct: use key to reset component identity
function PostPage({ postId }) {
  return <CommentThread key={postId} postId={postId} />;
}

function CommentThread({ postId }) {
  const [comments, setComments] = useState([]);
  // comments resets automatically when postId changes because key changed
  // ...
}

When key changes, React unmounts and remounts the component. The initial state runs fresh. No effect, no double render, no stale state window.

If you only want to reset part of the state, adjust the prop directly during render:

function CommentThread({ postId }) {
  const [comments, setComments] = useState([]);
  const [prevPostId, setPrevPostId] = useState(postId);

  if (postId !== prevPostId) {
    setComments([]);
    setPrevPostId(postId);
  }

  // ...
}

React will re-render immediately with the reset state before painting. It's unusual-looking but correct and faster than an effect.

Effect alternatives at a glance

SituationReach for effect?Better alternative
Compute a value from props/stateNoInline calculation or useMemo
Fire analytics on button clickNoEvent handler
Fetch data on mountRarelyuse() + Suspense or data library
Reset state when a prop changesNokey prop or render-time adjustment
Subscribe to a browser APIYesuseEffect with cleanup
Sync with a third-party DOM libraryYesuseEffect with ref
Initialize a singleton onceDebatableModule-level code or lazy init
Notify parent of state changeNoCall parent callback in event handler

When useEffect is genuinely the right tool

The cases where useEffect belongs are narrower than most codebases imply, but they're real.

Browser API subscriptions. Listening to resize, scroll, online/offline, or matchMedia events requires cleanup when the component unmounts. Effects handle this correctly.

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth);
    }
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return width;
}

Third-party library integration. A chart library, map widget, or rich text editor that imperatively controls a DOM node needs an effect to initialize and clean up.

WebSocket connections. Opening and closing a connection based on component lifecycle is exactly what effects exist for. The cleanup function is the entire point.

These are all cases of synchronizing with something outside React's render cycle. That's the correct use of useEffect. Everything else is worth questioning.

A decision tree for every effect you're tempted to write

Before writing useEffect, run through this:

  1. Can this value be computed from existing state or props? Compute during render.
  2. Does this logic run because a user did something? Put it in the event handler.
  3. Does this logic need to reset state when a prop changes? Use key or a render-time conditional.
  4. Does this fetch data that's available before or at render time? Use use() + Suspense.
  5. Does this subscribe to an external system and need cleanup? Now you need useEffect.

Step 5 is the only one that actually needs the hook. Steps 1-4 cover the majority of effects seen in real codebases.

At Laxaar, we've codified this as a lint rule in our custom software development projects: every useEffect in a PR has to pass a short checklist before it merges. The result is fewer bugs, fewer renders, and code that's far easier to read.

Frequently Asked Questions

Is it always wrong to fetch data in useEffect?

No. If you're not using Suspense or a data library, an effect with a cleanup flag (or AbortController) is still a workable pattern. The issue is that it's verbose and prone to race conditions. For new projects, we recommend TanStack Query or use() with Suspense. For existing codebases, incrementally adopting a data library beats a full rewrite.

What's the difference between useEffect and useLayoutEffect?

useEffect runs after the browser paints. useLayoutEffect runs synchronously after DOM mutations but before paint. Use useLayoutEffect only when you need to measure the DOM or apply style changes that would otherwise cause a visible flicker, for example positioning a tooltip. For everything else, useEffect is correct and causes less blocking.

Do I still need useEffect for authentication checks?

Usually not. Authentication state should come from context or a router that handles redirects at the navigation layer. Putting an auth check in an effect on every protected component creates a flash of unauthenticated content. A better pattern is a route guard: a wrapper component or layout that reads auth state from context and redirects before rendering children.

Does React 19's compiler change anything about useEffect?

The React Compiler automatically memoizes components and hooks to avoid unnecessary re-renders, which removes a class of effects people wrote to avoid expensive re-renders. It doesn't change when effects are appropriate. The compiler can't move logic from an effect to an event handler for you. The mental model question ("am I syncing with an external system?") is still what matters.

How do I handle initialization that should only run once?

If you need a side effect to run once when your app starts (setting up a third-party SDK, for instance), module-level code is often the right place, not an effect with an empty dependency array. Module code runs once when the module is first imported. An effect with [] runs once per component mount, which matters if the component ever unmounts and remounts.

// Runs once per app load, not per component mount
initAnalytics({ key: process.env.NEXT_PUBLIC_ANALYTICS_KEY });

export function App() {
  return <Routes />;
}

Fewer effects means fewer renders, fewer stale-closure bugs, and code your teammates can actually follow. If you're building a new product or untangling a component tree that's become hard to debug, the Laxaar team can help. Take a look at our web development and product engineering services, or get in touch to talk through your specific situation.

Working on something like this?

Get a fixed scope, timeline, and price within one business day — no obligation.

ReactuseEffectReact 19
Grow your business with us

Take your business to the next level.

Tell us what you're building. We'll come back inside one business day with a fixed scope, timeline, and team — or an honest “this isn't a fit”.

ENGINEERING PHILOSOPHY

Code is useless if it's not comprehensible to those who maintain it. We write code the next person can actually understand.