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.