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.