React Architecture01-08-202410 min read

Avoiding Modern React Anti-Patterns

A practical guide to recognizing and eliminating the most damaging anti-patterns in modern React applications.


Avoiding Modern React Anti-Patterns


Modern React encourages predictable rendering, stable component identity, and server-first data flow. Yet many developers still rely on outdated patterns that make applications harder to maintain, debug, and scale.

This article explains the most common anti-patterns in React today and shows how to replace them with clean, modern alternatives.


1. Fetching Data Inside useEffect When It Should Be Loaded on the Server


Anti-pattern


useEffect(() => {
  fetch("/api/user").then((res) => res.json()).then(setUser);
}, []);

This pushes data loading to the client, creates layout shifts, adds network latency, and complicates caching.


Modern solution


Load data on the server whenever possible.


// Server Component
export default async function Page() {
  const user = await getUser();
  return <UserProfile user={user} />;
}

Client components should only manage interaction, not data fetching.




2. Storing Server Data in Client State Management Tools


Anti-pattern


Using Redux, Zustand, or Context for data that originates from the backend.


const { user } = useUserStore();

This leads to stale caches, complicated invalidation, redundant API calls, and unnecessary re-renders.


Modern solution


Use React Query or server components to manage server data.


const { data } = useQuery({
  queryKey: ["user"],
  queryFn: fetchUser,
});

Client state ≠ server state.




3. Inline Objects, Arrays, and Functions Causing Re-Renders


Anti-pattern


<Chart options={{ dark }} />

Inline values create new identities every render.


Modern solution


Stabilize values with memoization.


const options = useMemo(() => ({ dark }), [dark]);
<Chart options={options} />;

Only memoize when the identity matters for child renders.




4. Defining Components Inside Other Components


Anti-pattern


function Page() {
  const Item = () => <div>Item</div>;
  return <Item />;
}

This redefines the component on every render, breaking identity and sometimes state.


Modern solution


Define components at the module level.


function Item() {
  return <div>Item</div>;
}

function Page() {
  return <Item />;
}

Stable identity reduces mental overhead and prevents subtle bugs.




5. Overusing useEffect for Logic That Belongs Elsewhere


Anti-pattern


Effects used as:


  • computed state
  • synchronization between states
  • triggering logic that should happen during render

Example:


useEffect(() => {
  setTotal(a + b);
}, [a, b]);

Modern solution


Compute directly during render.


const total = a + b;

Effects should be reserved for external systems: event listeners, subscriptions, browser APIs, or imperative sync.




6. Passing Context Values That Change Too Often


Anti-pattern


<ThemeContext.Provider value={{ dark }}></ThemeContext.Provider>

Creates a new value every render, forcing all consumers to re-render.


Modern solution


Stabilize the value.


const value = useMemo(() => ({ dark }), [dark]);
<ThemeContext.Provider value={value} />;

Or split contexts to avoid wide updates.




7. Using Context for Global State Instead of Scoped Stores


Context is slow for frequently changing values.


Anti-pattern


const { count } = useContext(CounterContext);

Every update re-renders all consumers.


Modern solution


Use a dedicated state library like Zustand for frequently updated slices.


const count = useCounterStore((s) => s.count);



8. Using useLayoutEffect Unnecessarily


Anti-pattern


useLayoutEffect(() => {
  console.log("mounted");
}, []);

This blocks the browser from painting and is rarely needed.


Modern solution


Use regular useEffect unless:


  • you must read layout synchronously
  • you must perform DOM measurement before paint

useLayoutEffect(() => {
  const { height } = ref.current.getBoundingClientRect();
}, []);

Most uses of useLayoutEffect are accidental.




9. Rendering Large Lists Without Virtualization


Anti-pattern


items.map((i) => <Row key={i.id} {...i} />);

Large lists destroy performance.


Modern solution


Use virtualization.


import { FixedSizeList } from "react-window";

Virtualization should be a default for datasets larger than a few hundred rows.




10. Treating React.memo as a Default Optimization Tool


Memoizing everything is a performance trap.


Anti-pattern


export default memo(function ListItem() { ... });

Unnecessary memoization adds CPU cost and increases complexity.


Modern solution


Use memo only when:


  • the component is expensive
  • the parent re-renders often
  • props are stable

Memo is a surgical tool, not a default pattern.




Final Thoughts


Avoiding anti-patterns is not about learning hacks.

It is about aligning your code with React’s rendering model:


  • keep identity stable
  • colocate state
  • eliminate unnecessary effects
  • load data on the server
  • keep client components focused on interaction

Cleaner patterns lead to faster performance and more predictable behavior in real applications.