DEV Community

kirandeepjassal-crypto
kirandeepjassal-crypto

Posted on • Originally published at prepstack.co.in

Top 10 Mistakes React Developers Make in 2026 — With Before/After Code, Production Metrics, and Real Fixes

Every React codebase I've audited over the last three years had the same 10 problems. Not different problems on different teams — the same 10. They cost real money: extra renders that drain mobile batteries, useEffect bombs that race and double-fetch, bundle bloat that adds full seconds to Largest Contentful Paint, accidental O(n²) updates that freeze typing inputs.

This is not "the 10 most clever React patterns." It's the 10 mistakes that show up over and over, with the exact before/after code, the production metrics from a real SaaS dashboard (a multi-tenant analytics platform with 800+ components, 18,000 LOC of TSX, 240k monthly active users), and the diagram for why each one matters.

This is a condensed cross-post. The full version — with every diagram, the diagnostic checklist, and the complete code — lives on my blog: Top 10 Mistakes React Developers Make in 2026.

TL;DR

# Mistake Cost when wrong Cost when fixed
1 Storing derived state with useState + useEffect Double renders, bugs on every dep change 0 extra renders
2 Fetching inside useEffect (waterfalls) LCP 4.2s LCP 1.4s
3 Stale closures + missing useEffect deps Random heisenbugs in prod Predictable behaviour
4 Index as key in dynamic lists Wrong rows highlighted; lost input state Stable rows
5 Context for high-frequency state Whole tree re-renders / keystroke One subscriber re-renders
6 Premature useMemo / useCallback Slower, harder to read No churn
7 State updates fired during render Infinite render loop in prod Clean control flow
8 Direct DOM mutation that fights React Reconciler resets your DOM Refs + state in sync
9 Giant components that re-render on every keystroke INP 380ms; typing visibly lags INP 80ms
10 Barrel imports + no code splitting JS bundle 720 KB JS 230 KB

Real production aggregate after fixing all 10:

  • LCP 4.2s → 1.4s (mobile, p75)
  • INP 380ms → 80ms (the biggest UX win)
  • JS bundle 720 KB → 230 KB
  • Re-renders per keystroke in main view: 47 → 3
  • Sentry React errors / week: 84 → 11
  • "App feels slow" support tickets: 31/month → 4/month

The fixes were not exotic. They were 10 boring habits applied consistently.


Mistake #1 — Storing derived state with useState + useEffect

This is the #1 mistake in every React codebase, full stop.

// ❌ store, then sync via effect
function CampaignsList({ campaigns, search }) {
  const [filtered, setFiltered] = useState([]);
  useEffect(() => {
    setFiltered(campaigns.filter(c => c.name.toLowerCase().includes(search.toLowerCase())));
  }, [campaigns, search]);
  return <Table rows={filtered} />;
}
Enter fullscreen mode Exit fullscreen mode

This renders twice on every prop change, shows a stale filter on first paint, and gives you two sources of truth.

// ✅ derive, don't store
const filtered = useMemo(
  () => campaigns.filter(c => c.name.toLowerCase().includes(search.toLowerCase())),
  [campaigns, search],
);
Enter fullscreen mode Exit fullscreen mode

Metric: /campaigns re-renders per keystroke: 6 → 2. INP on filtering: 220ms → 90ms.

The rule: If you can compute it from props/state during render, do that. State is for things React can't derive.


Mistake #2 — Fetching inside useEffect (the waterfall trap)

// ❌ fetch on mount in every component → sequential waterfall
useEffect(() => { fetch('/api/me').then(r => r.json()).then(setUser); }, []);
Enter fullscreen mode Exit fullscreen mode

Each fetch waits for the previous one and a render before it can even start.

// ✅ Server Component, parallel fetches
export default async function DashboardPage() {
  const user = await getUser();
  const [kpis, events, revenue] = await Promise.all([
    getKpis(user.id), getRecentEvents(user.id), getRevenue(user.id),
  ]);
  return <><KpiCards data={kpis} /><RecentEvents data={events} /><RevenueChart data={revenue} /></>;
}
Enter fullscreen mode Exit fullscreen mode

On a SPA, use TanStack Query with useQueries for parallel requests.

Metric: /dashboard LCP on 3G: 4,200ms → 1,400ms.

The rule: Don't fetch in components if you can fetch on the server. Don't fetch sequentially if you can fetch in parallel.


Mistake #3 — Stale closures and missing useEffect deps

// ❌ counter that "freezes"
useEffect(() => {
  const ws = new WebSocket(wsUrl);
  ws.onmessage = () => setCount(count + 1); // ← stale `count`
  return () => ws.close();
}, []); // ← missing dep
Enter fullscreen mode Exit fullscreen mode
// ✅ functional setState — always sees the latest value
ws.onmessage = () => setCount(c => c + 1);
Enter fullscreen mode Exit fullscreen mode

Turn react-hooks/exhaustive-deps to error, not warn.

Metric: "X stopped updating after Y action" bugs in Sentry: 23 → 1.


Mistake #4 — Index as key in dynamic lists

{campaigns.map((c, i) => <CampaignRow key={i} campaign={c} />)} // ❌
{campaigns.map(c => <CampaignRow key={c.id} campaign={c} />)}   // ✅
Enter fullscreen mode Exit fullscreen mode

When you delete a row, index keys make React reuse the wrong DOM nodes — inputs jump, focus lands wrong, animations replay. Index keys are safe only when the list never reorders, never has middle inserts/removes, and items hold no internal state.

Metric: row-state bugs ("wrong toggle flipped after sort"): 9 → 0.


Mistake #5 — Context for high-frequency state

Context re-renders all consumers on any value change. Put mousePos (60×/sec) in a shared context and your whole app re-renders 60 times a second.

// ✅ for many high-frequency values, use a subscribable store
const useStore = create((set) => ({
  cart: { items: [] },
  setMousePos: (pos) => set({ mousePos: pos }),
}));
function CartBadge() {
  const itemCount = useStore(s => s.cart.items.length); // re-renders only when count changes
  return <Badge>{itemCount}</Badge>;
}
Enter fullscreen mode Exit fullscreen mode

Metric: /inbox re-renders per second: 62 → 4 after moving to Zustand slices.

The rule: Context for low-frequency global values (theme, user, locale). Subscribable store for anything that updates more than once a second.


Mistake #6 — Premature useMemo and useCallback

useMemo has overhead. For a cheap string concat, the memo is slower than just computing it. Memoize only when (1) the computation is genuinely expensive, (2) the value is a dep of another hook, or (3) you're passing it to a React.memo'd child.

Metric: removed 84 useMemo + 67 useCallback calls → dashboard render time down 11%.

The rule: Default to no memo. Add it only when a profile shows the cost is real.


Mistake #7 — Updating state during render

// ❌ setState in the component body → second render / infinite loop
if (team && !name) setName(team.name);
Enter fullscreen mode Exit fullscreen mode
// ✅ derive a default; let the user override
const [draft, setDraft] = useState(null);
const name = draft ?? team?.name ?? '';
return <input value={name} onChange={e => setDraft(e.target.value)} />;
Enter fullscreen mode Exit fullscreen mode

The rule: Render must be pure. Never mutate state from the component body.


Mistake #8 — Fighting React with direct DOM manipulation

// ❌ querySelector + style mutation gets undone on re-render
const el = document.querySelector('.message-list');
if (el) el.scrollTop = el.scrollHeight;
Enter fullscreen mode Exit fullscreen mode
// ✅ ref-based, scoped, robust
const ref = useRef(null);
useEffect(() => {
  ref.current?.scrollTo({ top: ref.current.scrollHeight, behavior: 'smooth' });
}, [messages]);
Enter fullscreen mode Exit fullscreen mode

Direct DOM is fine for reading layout (getBoundingClientRect from a ref), focus management, and animation libs. Never for toggling classes/text React also manages.

Metric: /inbox scroll glitches: 5 reports/week → 0.


Mistake #9 — Giant components that re-render on every keystroke

When a search input lives in the same component as a 5,000-row table, every keystroke re-renders the table, the filters, and the pagination.

Three layered fixes: (a) isolate the input's state in a small component, (b) useDeferredValue for the expensive consumer, (c) virtualize the table.

const deferredSearch = useDeferredValue(search); // input updates now; table catches up
const filtered = useMemo(() => campaigns.filter(c => c.name.includes(deferredSearch)), [campaigns, deferredSearch]);
Enter fullscreen mode Exit fullscreen mode

Metric: /campaigns INP: 380ms → 80ms. Re-renders per keystroke: 47 → 3.


Mistake #10 — Barrel imports + no code splitting

import { Button, Card } from '@/components'; // ❌ may pull the entire UI library
import { Button } from '@/components/button'; // ✅ leaf import
Enter fullscreen mode Exit fullscreen mode

Diagnose with npx @next/bundle-analyzer. Leaf-import; dynamically import heavy below-the-fold components; swap heavy libs (momentdate-fns, lazy-load chart.js/mapbox-gl).

Metric: home route JS 720 KB → 230 KB, LCP 4.0s → 1.6s.


The aggregate (after fixing all 10)

Metric Before After Improvement
LCP (mobile p75) 4.2s 1.4s −67%
INP (overall p75) 380ms 80ms −79%
Home route JS 720KB 230KB −68%
Re-renders per keystroke 47 3 −94%
Sentry React errors / week 84 11 −87%
CrUX "Core Web Vitals: Good" 41% 89% +117%

Three weeks of work. No new features. Trial-signup conversion rose 14%. Speed converts.


Three habits that prevent 90% of the pain:

  1. Default to derivation over state. Store the minimum; derive the rest.
  2. Profile before you optimize. React DevTools Profiler, Lighthouse, bundle analyzer — not vibes.
  3. Make linting strict. react-hooks/exhaustive-deps: 'error'.

Read the full version with every diagram and the complete diagnostic checklist on PrepStack. Auditing a React codebase that's slowing down? Drop the page URL in the comments — happy to point at which of these 10 is likely the culprit.

Top comments (0)