RBA Consulting
RBA Consulting
RBA Consulting

TL;DR: Key Takeaways

  • Use useEffect primarily to synchronize React components with external systems.
  • Don’t store values in state when they can be derived from existing props or state.
  • Perform simple derived calculations during render and consider useMemo when the computation is non-trivial.
  • Avoid effect chains where one state update triggers another effect, which triggers another state update.
  • Put user-triggered logic in event handlers rather than effects.
  • Split large effects by responsibility and external system.
  • Keep source state minimal and derive everything else whenever possible.

The short version: fetch in effects, derive in render.

 

For enterprise development teams, maintainability often matters just as much as getting a feature to work. As React and React Native applications grow across teams, products, and years of development, small architectural decisions can compound into codebases that are increasingly difficult to understand, debug, and change safely.

One pattern I see contributing to that complexity again and again is the overuse of useEffect.

I spend a lot of time helping teams untangle React and React Native apps that feel “hard to reason about.” A common thread in many of those codebases is not React itself. It is overusing useEffect for logic that should be handled somewhere else.

useEffect is useful, but it is also one of the easiest hooks to misuse. The end result is often state chains, render loops, stale values, and bugs that only show up when timing changes.

In this post, we’ll walk through common useEffect code smells and how to refactor them into cleaner patterns.

 

What useEffect is for

At a high level, useEffect is for synchronizing your component with external systems:

  • Network calls
  • Browser APIs
  • Native device APIs
  • Event subscriptions
  • Timers

 

If your effect is only calculating data from props or state and then setting more state, that is usually a smell.

Smell #1: State-to-state chains inside effects

This is one of the most common patterns:

const [firstName, setFirstName] = useState("Ada");
const [lastName, setLastName] = useState("Lovelace");
const [fullName, setFullName] = useState("");

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

This works, but fullName is derived from existing state. You now have duplicated truth and extra rendering work.

Better approach

Compute derived values during render, assuming the computation is cheap and not an object or array reference that would cause unnecessary re-renders.

const fullName = `${firstName} ${lastName}`;

No extra state, no effect, no dependency management.

 

Smell #2: Effects used for data manipulation

Another common one is filtering/sorting/mapping in useEffect and storing it in state:

const [users, setUsers] = useState<User[]>([]);
const [search, setSearch] = useState("");
const [visibleUsers, setVisibleUsers] = useState<User[]>([]);

useEffect(() => {
  const next = users
    .filter((u) => u.name.toLowerCase().includes(search.toLowerCase()))
    .sort((a, b) => a.name.localeCompare(b.name));

  setVisibleUsers(next);
}, [users, search]);

This is pure computation.  No side effect is happening.

Better approach

Use useMemo when the computation is non-trivial:

const visibleUsers = useMemo(() => {
  return users
    .filter((u) => u.name.toLowerCase().includes(search.toLowerCase()))
    .sort((a, b) => a.name.localeCompare(b.name));
}, [users, search]);

And if the computation is cheap, just do it inline without useMemo.

 

Smell #3: Fetch -> setState -> effect chain

This one usually starts with good intent, then grows into a chain:

  1. Fetch data in one effect
  2. Set raw data to state
  3. Another effect transforms it
  4. Another effect calculates totals/counts
useEffect(() => {
  fetchOrders().then(setOrders);
}, []);

useEffect(() => {
  setShippedOrders(orders.filter((o) => o.status === "shipped"));
}, [orders]);

useEffect(() => {
  setShippedTotal(shippedOrders.reduce((sum, o) => sum + o.total, 0));
}, [shippedOrders]);

This is fragile and creates state dependency ladders. In a large enterprise application, patterns like this can become especially difficult to trace as more business rules, API dependencies, and developers are introduced.

Better approach

Keep only source data in state, derive everything else:

const [orders, setOrders] = useState<Order[]>([]);

useEffect(() => {
  let cancelled = false;

  async function loadOrders() {
    const next = await fetchOrders();
    if (!cancelled) setOrders(next);
  }

  loadOrders();
  return () => {
    cancelled = true;
  };
}, []);

const shippedOrders = useMemo(
  () => orders.filter((o) => o.status === "shipped"),
  [orders]
);

const shippedTotal = useMemo(
  () => shippedOrders.reduce((sum, o) => sum + o.total, 0),
  [shippedOrders]
);

Now the data flow is much easier to follow: fetch once, derive many.

 

Smell #4: Using an Effect Just to React to Local State Changes

You will sometimes see:

useEffect(() => {
  if (count > 10) {
    setWarning("High count");
  } else {
    setWarning("");
  }
}, [count]);

Again, this is derived UI state.

Better approach

const warning = count > 10 ? "High count" : "";

Use effects for external synchronization, not local bookkeeping.

 

Smell #5: Monster effects doing too much

If one effect is:

  • Fetching
  • Transforming data
  • Registering events
  • Updating document title
  • Writing to analytics

…it is trying to own too many responsibilities.

Better approach

Split effects by concern and keep each one tied to one external system.

That gives you:

  • Easier dependencies
  • Easier cleanup
  • Easier debugging

This separation becomes even more important as applications scale. Clear effect boundaries make it easier for multiple developers to understand what a component is doing without having to unravel a single block of unrelated behavior.

 

 

A Practical Rule I use

When I review code, I usually ask:

“If I remove this effect, does anything external break?”

If the answer is no, there is a high chance the effect is unnecessary and the logic belongs in render, useMemo, or an event handler.

It is a simple question, but it catches a surprising number of unnecessary effects.

 

Quick decision guide

Scenario
Use
Fetching/subscribing/timers/native APIs
useEffect
Deriving values from existing props/state
Inline computation or useMemo
User-triggered logic
Event handler

Final thoughts

useEffect is not bad.  It is just easy to overuse.

Most of the painful React code I see comes from effect chains that try to model data flow through state updates.  If you keep source state minimal and derive everything else, your components become easier to read, test, and maintain.

For enterprise teams, that simplicity has an impact beyond an individual component. Cleaner React and React Native patterns can reduce debugging time, make applications easier to extend, and help engineering teams maintain consistency as products and development teams scale.

The short version: fetch in effects, derive in render.

At RBA, our engineering teams help organizations modernize and improve complex digital applications across web and mobile, including React and React Native solutions. Whether the challenge is application architecture, performance, modernization, or an increasingly difficult-to-maintain codebase, we help teams identify the patterns creating unnecessary complexity and build a cleaner path forward.

Need help simplifying or modernizing a React or React Native application? Connect with RBA to explore how our application development and modernization teams can help.

Disclaimer

The information provided on this website is for general informational purposes only. While we strive to keep the content accurate and up-to-date, RBA, Inc., makes no representations or warranties of any kind, express or implied, about the completeness, reliability, or suitability of the information contained on this website.

Please note that RBA, Inc., is not a law firm, and its consultants are not attorneys or legal professionals. Any advice or opinions provided are offered in good faith and should not be construed as legal advice. We strongly recommend consulting your legal, regulatory, compliance, and/or security teams before making decisions with legal implications.

RBA, Inc., disclaims any liability for any loss or damage arising out of the use of this website or reliance on its content.

About the Author

Adam Utsch
Adam Utsch

Senior Principal Consultant

Adam is a seasoned software professional with deep experience in development, deployment, and application support. With a strong engineering foundation, they specialize in building scalable solutions and mentoring others in the technologies that drive real impact. Adam is passionate about continuous improvement, collaboration, and staying ahead of the tech curve.