B / React blog

You Probably Don't Need That useEffect

Five common React patterns where useEffect is unnecessary and how to refactor each one into simpler, more predictable code.

Focus
React architecture
Published
Estimated reading time
14 min read
  • React patterns
  • useEffect
  • State management
  • Refactoring

useEffect is one of the most frequently misunderstood React APIs.

It often becomes the default solution whenever a component needs to do something after rendering: calculate a value, update another piece of state, respond to a button click, reset a form, or keep two state variables synchronized. The problem is that many of these situations do not require an Effect at all.

Using useEffect unnecessarily can introduce extra renders, temporary inconsistent UI, more complicated data flow, dependency-array bugs, infinite loops, state that can become stale, and code that is harder to understand and test.

When the logic only involves React props, state, rendering, or user events, you can usually solve it without an Effect.

This guide covers five common situations where useEffect is unnecessary and shows how to refactor each one.

B-01

1. Derived State

Derived state is a value that can be calculated from existing props or state. Imagine a shopping cart that stores both its items and its total price.

Before: Calculating derived state in an Effect

import { useEffect, useState } from "react";

type CartItem = {
  id: string;
  name: string;
  price: number;
  quantity: number;
};

type CartProps = {
  items: CartItem[];
};

export function Cart({ items }: CartProps) {
  const [total, setTotal] = useState(0);

  useEffect(() => {
    const nextTotal = items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );

    setTotal(nextTotal);
  }, [items]);

  return <p>Total: ${total.toFixed(2)}</p>;
}

This works, but the component renders twice when items changes. React first renders with the old total, then the Effect runs and updates total, then React renders again with the correct value. The component also stores duplicated information: total is not independent state, it is entirely determined by items.

After: Calculate the value during rendering

type CartItem = {
  id: string;
  name: string;
  price: number;
  quantity: number;
};

type CartProps = {
  items: CartItem[];
};

export function Cart({ items }: CartProps) {
  const total = items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  return <p>Total: ${total.toFixed(2)}</p>;
}

Now there is no extra state, no Effect, no second render, and no risk of items and total becoming inconsistent. The total is always correct because it is calculated from the current items during rendering.

What about expensive calculations?

If the calculation is genuinely expensive, memoize the calculation rather than synchronizing it through state.

import { useMemo } from "react";

export function Cart({ items }: CartProps) {
  const total = useMemo(() => {
    return items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );
  }, [items]);

  return <p>Total: ${total.toFixed(2)}</p>;
}

Do not reach for useMemo automatically. Most array transformations are fast enough to run during rendering. Use memoization only when the calculation has a measurable performance cost.

Refactoring rule

If a value can be calculated from current props or state, calculate it during rendering. Do not store it in state.

B-02

2. Transforming Props

Another common pattern is receiving data through props, transforming it, and saving the result in state. Consider a product list that needs to display only available products.

Before: Copying transformed props into state

import { useEffect, useState } from "react";

type Product = {
  id: string;
  name: string;
  price: number;
  inStock: boolean;
};

type ProductListProps = {
  products: Product[];
};

export function ProductList({ products }: ProductListProps) {
  const [availableProducts, setAvailableProducts] = useState<Product[]>([]);

  useEffect(() => {
    setAvailableProducts(
      products.filter((product) => product.inStock)
    );
  }, [products]);

  return (
    <ul>
      {availableProducts.map((product) => (
        <li key={product.id}>
          {product.name} — ${product.price}
        </li>
      ))}
    </ul>
  );
}

availableProducts is not independent state. It is a transformed version of products. This creates two sources of truth that must be manually kept synchronized.

After: Transform props during rendering

export function ProductList({ products }: ProductListProps) {
  const availableProducts = products.filter(
    (product) => product.inStock
  );

  return (
    <ul>
      {availableProducts.map((product) => (
        <li key={product.id}>
          {product.name} — ${product.price}
        </li>
      ))}
    </ul>
  );
}

The data flow is now direct: products becomes filter, then rendered list. There is no synchronization step in the middle.

Filtering and sorting together

The same principle applies when multiple transformations are required.

Before

const [visibleProducts, setVisibleProducts] = useState<Product[]>([]);

useEffect(() => {
  const result = products
    .filter((product) =>
      product.name.toLowerCase().includes(search.toLowerCase())
    )
    .sort((a, b) => a.price - b.price);

  setVisibleProducts(result);
}, [products, search]);

After

const visibleProducts = products
  .filter((product) =>
    product.name.toLowerCase().includes(search.toLowerCase())
  )
  .sort((a, b) => a.price - b.price);

Be careful with .sort(): it mutates the array it is called on. Since .filter() returns a new array, sorting its result is safe. When sorting props directly, copy the array first: const sortedProducts = [...products].sort((a, b) => a.price - b.price).

For expensive transformations, memoize the result with useMemo.

Refactoring rule

Do not copy props into state just to filter, sort, map, format, or group them. Transform props during rendering.

B-03

3. Event-Driven Logic

Effects run because a component rendered. Event handlers run because a specific user interaction occurred. That distinction matters.

Suppose you want to display a notification after a user adds a product to their cart.

Before: Reacting to event state through an Effect

import { useEffect, useState } from "react";

export function ProductActions() {
  const [addedProduct, setAddedProduct] = useState<string | null>(null);

  useEffect(() => {
    if (!addedProduct) {
      return;
    }

    console.log(`${addedProduct} was added to the cart`);
  }, [addedProduct]);

  function handleAddToCart(productName: string) {
    setAddedProduct(productName);
  }

  return (
    <button onClick={() => handleAddToCart("Mechanical Keyboard")}>
      Add to cart
    </button>
  );
}

The code creates state only to trigger an Effect. But the application already knows exactly when the product was added: inside handleAddToCart.

After: Keep event logic in the event handler

export function ProductActions() {
  function handleAddToCart(productName: string) {
    console.log(`${productName} was added to the cart`);
  }

  return (
    <button onClick={() => handleAddToCart("Mechanical Keyboard")}>
      Add to cart
    </button>
  );
}

The relationship between the action and its consequence is now obvious.

A more realistic example

Imagine submitting an order and showing a toast.

Before

const [orderSubmitted, setOrderSubmitted] = useState(false);

useEffect(() => {
  if (orderSubmitted) {
    toast.success("Your order was submitted");
  }
}, [orderSubmitted]);

async function handleSubmit() {
  await submitOrder();
  setOrderSubmitted(true);
}

After

async function handleSubmit() {
  await submitOrder();
  toast.success("Your order was submitted");
}

The notification belongs to the submission event. Using an Effect here creates unnecessary indirection: user submits, then state changes, then component renders, then Effect notices the state, then notification appears. The direct version is simpler: user submits, then order is submitted, then notification appears.

Avoid moving every function into an Effect

A common misconception is that side effects such as API calls must always happen inside useEffect. That is not true. An API call caused by rendering may belong in an Effect or a data-fetching framework. An API call caused by a user action belongs in the event handler.

async function handleDeleteAccount() {
  const confirmed = window.confirm(
    "Are you sure you want to delete your account?"
  );

  if (!confirmed) {
    return;
  }

  await deleteAccount();
  navigate("/goodbye");
}

The action is event-driven, so it does not need an Effect.

Refactoring rule

Ask why the logic should run. If it should run because the user clicked, submitted, selected, dragged, or typed something, put it in the relevant event handler. Do not represent an event as state and then watch that state with an Effect.

B-04

4. Resetting State

Resetting state when a prop changes is a common reason developers add Effects. Consider a comment form that should clear whenever the selected article changes.

Before: Resetting state in an Effect

import { useEffect, useState } from "react";

type CommentFormProps = {
  articleId: string;
};

export function CommentForm({ articleId }: CommentFormProps) {
  const [comment, setComment] = useState("");

  useEffect(() => {
    setComment("");
  }, [articleId]);

  return (
    <textarea
      value={comment}
      onChange={(event) => setComment(event.target.value)}
    />
  );
}

When articleId changes, React first renders the old component state for the new article. Then the Effect runs and clears the comment. That creates an unnecessary render and briefly associates the old comment with the new article.

After: Reset the component using a key

type CommentSectionProps = {
  articleId: string;
};

export function CommentSection({ articleId }: CommentSectionProps) {
  return (
    <CommentForm
      key={articleId}
      articleId={articleId}
    />
  );
}

function CommentForm({ articleId }: CommentFormProps) {
  const [comment, setComment] = useState("");

  return (
    <form>
      <label htmlFor={`comment-${articleId}`}>Comment</label>

      <textarea
        id={`comment-${articleId}`}
        value={comment}
        onChange={(event) => setComment(event.target.value)}
      />
    </form>
  );
}

A different key tells React that this is a different component instance. When articleId changes, React removes the previous form and creates a fresh one with fresh state. This is usually the cleanest way to reset all state inside a subtree.

Resetting only part of the state

Sometimes you do not want to reset the entire component. Imagine a product page where the selected image should reset when the product changes, but the zoom preference should remain. You could move the product-specific state into a keyed child.

type ProductPageProps = {
  product: Product;
};

export function ProductPage({ product }: ProductPageProps) {
  const [zoomEnabled, setZoomEnabled] = useState(false);

  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={zoomEnabled}
          onChange={(event) => setZoomEnabled(event.target.checked)}
        />
        Enable zoom
      </label>

      <ProductGallery
        key={product.id}
        product={product}
        zoomEnabled={zoomEnabled}
      />
    </>
  );
}

Now ProductGallery resets when the product changes, but zoomEnabled remains because it belongs to the parent. The component structure communicates which state belongs to which identity.

Another option: Store stable identifiers

Sometimes developers store a selected object and then reset it when the collection changes.

Before

const [selectedProduct, setSelectedProduct] =
  useState<Product | null>(null);

useEffect(() => {
  setSelectedProduct(null);
}, [products]);

A better design may be to store the selected ID instead of the selected object.

const [selectedProductId, setSelectedProductId] =
  useState<string | null>(null);

const selectedProduct =
  products.find((product) => product.id === selectedProductId) ?? null;

Now, if the selected product disappears from the collection, selectedProduct naturally becomes null. There is no reset Effect to maintain.

Refactoring rule

When state should reset because the identity of something changed, consider giving the component a different key, moving the state into a keyed child, or storing a stable identifier instead of duplicating an entire object.

B-05

5. Synchronizing React State with React State

One of the biggest warning signs in a React component is an Effect whose only purpose is updating one state variable when another state variable changes.

Before: Synchronizing two pieces of state

import { useEffect, useState } from "react";

export function RegistrationForm() {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [fullName, setFullName] = useState("");

  useEffect(() => {
    setFullName(`${firstName} ${lastName}`.trim());
  }, [firstName, lastName]);

  return (
    <form>
      <input
        value={firstName}
        onChange={(event) => setFirstName(event.target.value)}
        placeholder="First name"
      />

      <input
        value={lastName}
        onChange={(event) => setLastName(event.target.value)}
        placeholder="Last name"
      />

      <p>Welcome, {fullName}</p>
    </form>
  );
}

fullName is synchronized with firstName and lastName, but it does not need to be state.

After: Calculate the synchronized value

export function RegistrationForm() {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");

  const fullName = `${firstName} ${lastName}`.trim();

  return (
    <form>
      <input
        value={firstName}
        onChange={(event) => setFirstName(event.target.value)}
        placeholder="First name"
      />

      <input
        value={lastName}
        onChange={(event) => setLastName(event.target.value)}
        placeholder="Last name"
      />

      <p>Welcome, {fullName}</p>
    </form>
  );
}

There is now a single source of truth.

Keeping filters synchronized

Suppose a dashboard has a selected country and selected city. When the country changes, the city should be cleared.

Before: Watching one state variable to update another

const [country, setCountry] = useState("");
const [city, setCity] = useState("");

useEffect(() => {
  setCity("");
}, [country]);

This is better handled where the country changes.

After: Update related state in the same event

const [country, setCountry] = useState("");
const [city, setCity] = useState("");

function handleCountryChange(nextCountry: string) {
  setCountry(nextCountry);
  setCity("");
}

Both updates belong to the same user interaction, so keeping them together makes the behavior explicit.

Consider combining related state

When multiple state variables always change together, a reducer or a single state object may better express the relationship.

type LocationState = {
  country: string;
  city: string;
};

const [location, setLocation] = useState<LocationState>({
  country: "",
  city: "",
});

function handleCountryChange(country: string) {
  setLocation({
    country,
    city: "",
  });
}

function handleCityChange(city: string) {
  setLocation((current) => ({
    ...current,
    city,
  }));
}

For more complex transitions, use a reducer.

type LocationState = {
  country: string;
  city: string;
};

type LocationAction =
  | { type: "countryChanged"; country: string }
  | { type: "cityChanged"; city: string };

function locationReducer(
  state: LocationState,
  action: LocationAction
): LocationState {
  switch (action.type) {
    case "countryChanged":
      return {
        country: action.country,
        city: "",
      };

    case "cityChanged":
      return {
        ...state,
        city: action.city,
      };

    default:
      return state;
  }
}

This makes valid state transitions explicit instead of relying on Effects to repair state after rendering.

Refactoring rule

Do not use Effects to make React state catch up with other React state. Instead, derive the value during rendering, update related state in the same event, combine state that always changes together, or use a reducer for complex transitions.

B-06

Why Unnecessary Effects Cause Problems

Unnecessary Effects are not merely a style preference. They can create real bugs.

1. They create extra render cycles

An Effect cannot update state until after React renders. The sequence becomes: render with stale value, then Effect runs, then state updates, then render again. When the value could have been calculated during rendering, the first render was unnecessary.

2. They create temporary inconsistent states

Suppose a product changes, but the selected image is reset inside an Effect. For one render, the component may combine the new product with the old selected image. Even if the user never notices the intermediate state, your component still has to handle it correctly.

3. They duplicate sources of truth

If fullName is state and is also determined by firstName and lastName, which one is authoritative? Every duplicated value introduces a synchronization responsibility.

4. They hide causality

This code is direct: handleSave calls saveDocument and then showSuccessMessage. This code makes the relationship harder to follow: handleSave sets shouldSave to true, and a separate Effect watches shouldSave to call saveDocument and showSuccessMessage. The first version tells you what happens when the user saves. The second requires tracing state changes across different parts of the component.

5. They make dependency management harder

Once logic is inside an Effect, you must manage every reactive dependency it reads. Missing a dependency can produce stale values. Adding a dependency can make the Effect run more often than expected. Trying to silence the linter often hides the underlying design problem instead of solving it.

B-07

When You Actually Need useEffect

The conclusion is not that useEffect is bad. It is essential when a component must synchronize with something outside React.

Browser APIs

useEffect(() => {
  document.title = `${unreadCount} unread messages`;
}, [unreadCount]);

Event subscriptions

useEffect(() => {
  function handleOnline() {
    setIsOnline(true);
  }

  function handleOffline() {
    setIsOnline(false);
  }

  window.addEventListener("online", handleOnline);
  window.addEventListener("offline", handleOffline);

  return () => {
    window.removeEventListener("online", handleOnline);
    window.removeEventListener("offline", handleOffline);
  };
}, []);

Third-party widgets

useEffect(() => {
  const map = createMapWidget(containerRef.current);

  return () => {
    map.destroy();
  };
}, []);

Network or realtime connections

useEffect(() => {
  const connection = createChatConnection(roomId);

  connection.connect();

  return () => {
    connection.disconnect();
  };
}, [roomId]);

These Effects synchronize the component with systems whose lifecycle exists outside React. That is what Effects are designed for.

For server data, consider using the data-loading mechanism provided by your framework or a dedicated server-state library. Fetching directly in Effects can be valid, but it requires handling cancellation, race conditions, caching, loading states, and duplicate requests carefully.

B-08

A Practical Refactoring Checklist

Before writing an Effect, ask these questions.

Can I calculate this value during rendering?

If yes, remove the state and calculate the value directly: const fullName = `${firstName} ${lastName}`.

Am I transforming props?

Filter, map, sort, group, or format them during rendering: const visibleItems = items.filter(matchesSearch).

Did a specific user action cause this logic?

Put the logic in the event handler.

function handleSubmit() {
  submitForm();
  showSuccessToast();
}

Should state reset when an entity changes?

Use a key, move the state into a keyed child, or store an identifier.

<Editor key={documentId} documentId={documentId} />

Am I updating one state variable because another changed?

Derive the value, update both in the same event, or redesign the state model.

function handleCountryChange(country: string) {
  setCountry(country);
  setCity("");
}

Am I synchronizing with something outside React?

This is where an Effect is likely appropriate. Examples include browser APIs, WebSocket connections, DOM integrations, timers, analytics, third-party libraries, and external subscriptions.

B-09

The Mental Model That Changes Everything

Do not ask how to make this Effect work. Ask why this logic needs to happen.

There are three common answers.

  • Because the component rendered: calculate the value during rendering.
  • Because the user did something: run the logic in an event handler.
  • Because an external system must stay synchronized: use an Effect.

That distinction eliminates a surprising number of Effects.

B-10

Final Takeaway

useEffect should not be the glue holding your React state together. When React state needs to be synchronized with other React state, the component's data model is often the real problem.

Prefer calculating instead of synchronizing, event handlers instead of event-watching Effects, keys instead of manual resets, single sources of truth instead of duplicated state, and reducers instead of state-repair logic.

Effects are an escape hatch for interacting with systems outside React. The fewer unnecessary Effects your components contain, the more predictable your application becomes.

Before adding your next useEffect, pause and ask: is React synchronizing with an external system, or am I using an Effect to compensate for state I did not need?

B-11

Where this fits

Recognizing unnecessary Effects and replacing them with simpler patterns is part of building React codebases that teams can own and evolve confidently. The refactoring discipline in this article reduces extra renders, simplifies data flow, and makes component behavior easier to reason about—outcomes that matter in every engagement.

Explore Stronger React TeamsSee how foundations shape delivery