Hi! Send me a message
Contact

Mexico City, Mexico
Available for remote and on-site projects.

Professional networks

LinkedIn · GitHub · Email
Applied engineering, measurable results.

Common useEffect mistakes and how to fix them (without losing SEO when migrating to React)

· 9 min read · Samuel Torres

Five-petal polar rose
r(t) = cos(5t)

In short

  • An effect runs after paint and only in the browser: anything that depends on it is missing from the initial HTML.
  • Every request inside an effect needs cancellation.
  • If a value can be computed during render, it is not state and needs no effect.
  • Title, description and canonical do not belong in useEffect: in React 19 you declare them in the component.
  • When several useState values change together, it is time for useReducer.

React useEffect mistakes are the most frequent cause of bugs that are hard to reproduce: stale data on screen, chained renders, infinite loops. In an internal app they are a nuisance. On a site that lives on organic traffic, they cost rankings.

I work on enterprise migrations to React that have to preserve SEO. In that context, useEffect stops being a style topic and becomes a concrete question: which part of the content exists in the HTML before JavaScript runs?

This article covers the four most common mistakes, how to diagnose them, and when the real problem is how you model state. The examples are in TypeScript and are illustrative.

Why useEffect weighs more in an SEO-sensitive migration

A WordPress or PHP site delivers the full HTML from the server. The title, the heading and the content are already there when the response arrives.

When you migrate to React, that guarantee disappears unless you protect it. An effect has two properties that break it:

  • It runs after paint. React shows the component first and runs the effect afterwards. Whatever the effect changes arrives in a second render.
  • It does not run on the server. During server rendering, effects are not executed. Everything that depends on them is left out of the initial HTML.

The consequence is direct. If the main content or the metadata are resolved inside an effect, the HTML a crawler receives is incomplete, even though everything looks fine in your browser.

That is why the first rule of an SEO-sensitive migration is architectural: the data that defines the page is resolved on the server. Effects are left for what actually is an effect: synchronizing with something outside React.

The 4 most common useEffect mistakes

1. Fetching data without cancelling the previous request

The user types in a search box. Every keystroke fires a request. Responses do not arrive in order, and the one from an old search can overwrite the most recent one.

It is a race condition, and it almost never shows up in development because the local network is fast. The fix is for every effect to cancel its own request on cleanup:

useEffect(() => {
  const ctrl = new AbortController();

  // Without cancellation, a stale response can overwrite the new one.
  fetch(`/api/products?q=${encodeURIComponent(q)}`, { signal: ctrl.signal })
    .then((r) => r.json())
    .then(setProducts)
    .catch((e) => {
      // Aborting is part of the normal flow, not an error to display.
      if (e.name !== "AbortError") setError(e);
    });

  // Runs when q changes and on unmount: nothing is left hanging.
  return () => ctrl.abort();
}, [q]);

If the request defines the main content of the page, the real fix is different: take it out of the effect and resolve it on the server, or with a data library that handles caching and cancellation.

2. Storing derived state with an effect

This is the most widespread mistake. A value that can be computed from props or other state gets its own useState, and an effect "syncs" it.

The result is an extra render on every change and a window in which the screen shows two values that do not match.

// Bad: duplicated state and an extra render for every change in items.
const [total, setTotal] = useState(0);
useEffect(() => {
  setTotal(items.reduce((s, i) => s + i.price * i.quantity, 0));
}, [items]);

// Good: computed during render. There is nothing to synchronize.
const total = useMemo(
  () => items.reduce((s, i) => s + i.price * i.quantity, 0),
  [items],
);

The question that catches it is simple: can I compute this from what I already have? If the answer is yes, it is not state. And useMemo is only needed when the computation is expensive; for a short sum, a plain constant is enough.

3. Dependencies that lie

The dependency array is a contract: it tells React which values the effect depends on. There are two ways to break it.

The first is leaving out a dependency "so it doesn't fire so often". The effect is left holding a stale captured value, and the bug shows up weeks later, far from its cause.

The second is depending on an object or function that is created on every render. To React it is a new value each time, so the effect always runs. If it also updates state, you have an infinite loop.

  • Depend on primitive values. Instead of [filters], use [filters.category, filters.page].
  • Move inside the effect whatever only the effect uses. A function declared inside does not need to be in the dependencies.
  • Use the functional form of setState. setN((n) => n + 1) does not need to read n from outside.

4. Resolving critical content and metadata in an effect

This is the mistake that costs the most in a migration. It looks harmless: setting document.title or injecting the description inside a useEffect.

Because effects do not run on the server, the initial HTML ships with the app's generic title. Every page shares the same title until JavaScript runs.

React 19 solves it without libraries: <title>, <meta> and <link> tags are declared in the component and React places them in the <head>, during server rendering too.

// Bad: the initial HTML ships without its own title or description.
useEffect(() => {
  document.title = product.name;
}, [product]);

// Good (React 19): the metadata travels in the server HTML.
export function ProductPage({ product }: { product: Product }) {
  return (
    <article>
      <title>{`${product.name} | Store`}</title>
      <meta name="description" content={product.summary} />
      <link rel="canonical" href={product.url} />
      <h1>{product.name}</h1>
    </article>
  );
}

The same rule applies to content. The main heading, the copy and the internal links must be in the first HTML. An effect may enrich the page; it must not build it.

How to diagnose useEffect problems

Most of these mistakes are caught before production if the team uses four tools it already has at hand.

  • Strict Mode. In development, React mounts, cleans up and remounts every component. If an effect breaks when it runs twice, it is missing cleanup. Do not turn Strict Mode off to "fix" it.
  • The exhaustive-deps rule. The ESLint plugin for hooks flags every omitted dependency. Treat it as an error, not a warning.
  • The React DevTools Profiler. It shows which components render and why. Two renders in a row for every interaction usually give away derived state.
  • The HTML without JavaScript. It is the test that matters most for SEO, and the most ignored.

That last test can be automated in three lines. Whatever is missing from this response does not exist for a crawler that does not execute JavaScript:

# Download the HTML exactly as the server delivers it, without running JavaScript.
curl -s https://your-site.example/product/sample/ > initial.html

# Every page must carry its own title, a single h1 and its canonical.
grep -o "<title>[^<]*</title>" initial.html
grep -c "<h1" initial.html
grep -o '<link rel="canonical"[^>]*>' initial.html

In a migration, this check runs against the full list of URLs, before and after the change. If a page loses its title or its heading in the initial HTML, the deploy stops.

Recommended practices for useState and useReducer

Many problematic effects are a symptom of something else: the state is badly modeled. When an effect exists only to keep several useState values coordinated, the effect is not the disease.

The typical case is a data load with three separate states: loading, error and data. Nothing prevents loading and error from being true at the same time. Sooner or later that impossible state gets painted.

A reducer with a discriminated union removes those combinations at the type level:

type State =
  | { phase: "idle" }
  | { phase: "loading" }
  | { phase: "ready"; data: Product[] }
  | { phase: "error"; message: string };

type Action =
  | { type: "request" }
  | { type: "ok"; data: Product[] }
  | { type: "fail"; message: string };

// Every transition lives here: testable without mounting a component.
function reducer(_: State, a: Action): State {
  switch (a.type) {
    case "request": return { phase: "loading" };
    case "ok":      return { phase: "ready", data: a.data };
    case "fail":    return { phase: "error", message: a.message };
  }
}

With this model, the compiler will not let you read data unless the phase is ready. A whole class of bugs stops being possible.

The practical rule for choosing:

  • useState for independent values: a text field, a toggle, an active tab.
  • useReducer when several values change together or when the next state depends on the action and the previous state.
  • Neither for server data. That is a cache, and it deserves a tool built for caching.
  • The URL for whatever must survive a reload or a share: filters, page, sort order. It is also what a search engine can link to.

There is a point where even useReducer is not enough: when the same state is needed in distant branches of the tree. That is when you lift it into a context or an external store. But that decision is made last, not first.

What NOT to do

  • Silence the dependency rule. A comment that disables exhaustive-deps does not fix the effect; it hides the bug until someone changes the surrounding code.
  • Use useEffect as "on mount" for everything. Fetching data, computing values and setting metadata on mount replicates the class component model. There is almost always a better option: server, render, or event handler.
  • Validate the migration only in the browser. A page that looks right with JavaScript tells you nothing about the initial HTML. Without the raw HTML test, you discover the problem in the traffic report.
  • Turn off Strict Mode. The double mount in development is a free test. If it bothers you, it is because it found something.

The next step

Fixing useEffect mistakes is not about memorizing rules. It is about deciding where each thing lives: page data on the server, computations in render, transitions in a reducer, and effects only for synchronizing with the outside world.

If your team is facing a migration to React and cannot afford to lose rankings, or needs to audit the performance of a frontend already in production, at Conqr Digital Agency we design architectures that scale. Let's review your case.

If you want to know how I approach projects like this, take a look at the custom web development service.

FAQ

Why does my useEffect run twice?
In development, React's Strict Mode mounts the component, runs the effect, cleans it up and runs it again. It does this on purpose, to expose effects with no cleanup. In production it runs once.
Does useEffect affect SEO?
Indirectly, yes. Effects only run in the browser, after the first paint. If the main content, the title or the description depend on an effect, they are not in the HTML the server delivers.
When should I use useReducer instead of useState?
When several values change together, when the next state depends on the previous one and on the action, or when there are state combinations that should never exist. A reducer keeps the transitions in a single place.
Samuel Torres Pichardo

Samuel Torres Pichardo
Software and cloud engineer in Mexico City, with about 3 years of professional experience.
LinkedIn · GitHub · Contact

Professional contact:

Mexico City, Mexico

Available for remote and on-site projects.
B.Eng. in Computing and Telecommunications Technologies

© 2026 Eng. Samuel Torres. Applied Engineering.