What are common mistakes developers make in production React applications?
Top hits: server data in Redux (reinventing React Query); over-memoization; missing keys / unstable keys (causing remounts); huge bundle from named-namespace imports; CSR for content pages; missing error boundaries; useEffect dep arrays wrong (stale closures or missing deps); state too high in tree; not handling loading/error/empty states; no a11y; no error monitoring; no perf budget; lazy-loading the LCP image.
Patterns I see go wrong repeatedly in production React codebases.
1. Server data in Redux/Zustand instead of React Query
Half the components fetch in useEffect, dispatch to Redux, then read back. Caching, dedup, refetch all hand-rolled — usually badly. React Query / RTKQ / SWR solve this once. Use them.
2. Over-memoization
useCallback and useMemo everywhere "for perf." Each one adds overhead (creation, dep comparison, GC). Most don't pay back. The right rule: profile first; memoize only when the child is memoized and the prop is genuinely expensive to recreate.
3. Under-memoization where it matters
The flip side: a heavy parent re-renders 60 times/sec because of inline style/function props, and the memo'd children never benefit. Stable references matter.
4. Missing or unstable keys
{items.map((item, i) => <Row key={i} />)} // index key — bad on reorder
{items.map(item => <Row key={Math.random()} />)} // regenerated — full remount every renderUse stable, unique ids. Index keys are fine for static lists but break on reorder.
5. CSR for content pages
Marketing pages and blogs as CSR React → bad SEO, broken social previews, slow LCP. Use SSR/SSG for any page where SEO/share matters.
6. No error boundaries
One thrown error in a child → blank screen for the whole app. Wrap routes and major widgets in error boundaries with sensible fallbacks.
7. useEffect with wrong dep arrays
useEffect(() => {
fetch(url).then(/* ... */);
}, []); // missing url dependency → stale closureOr the inverse: deps that include unstable refs → effect fires every render. Use the eslint-plugin-react-hooks rule.
8. State too high in the tree
Auth + theme + every modal's open state + every filter — all in app-level Context. Every state change cascades through the entire app. Lift state down to where it's used; split global state into independent slices.
9. No loading / error / empty states
{data && <List items={data} />} — blank screen during load, blank screen on error, blank screen on empty. Handle all four states (loading, error, empty, success).
10. Massive bundle from namespace imports
import _ from 'lodash'; // 75KB ships
import * as icons from 'lucide-react'; // all icons shipPer-function imports + tree-shakeable libs.
11. Inline object/function props busting memo
<MemoizedChild style={{ margin: 8 }} onClick={() => handle()} />
// fresh references each render — memo never firesHoist or useCallback/useMemo.
12. Lazy-loading the LCP image
loading="lazy" on the hero image → LCP tanks. Eager-load + preload + fetchpriority=high.
13. dangerouslySetInnerHTML with user input
XSS vector. Use DOMPurify if HTML is unavoidable.
14. No a11y
<div onClick> instead of <button>, no keyboard support, no focus management in modals, no labels on inputs, no alt on images. Most are zero-effort fixes; together they make the app unusable for many users.
15. Storing tokens in localStorage
XSS-readable. Use HttpOnly cookies with SameSite=Lax + Secure.
16. No CI perf budget
size-limit or Lighthouse CI not set up. Bundle grows by 10KB per week unnoticed.
17. No error monitoring
Production errors invisible. Wire up Sentry / Datadog / Honeybadger; source-map deploys so stacks are readable.
18. Not handling unmount in async work
useEffect(() => {
fetch(url).then(setData);
return () => /* nothing */;
}, [url]);If the component unmounts before fetch resolves, setData warns. Use AbortController + cleanup.
19. Strict Mode treated as a bug instead of a feature
React 18 Strict Mode double-invokes effects/renders in dev. Devs sometimes work around it; usually it's surfacing a real bug (effect not idempotent, side effect in render).
20. Forgetting Suspense around React.lazy
React.lazy without Suspense throws.
21. Treating SPA as "good enough" for SEO
Googlebot runs JS but with a budget; other crawlers don't. SSR/SSG for indexable content.
22. Polyfilling for browsers you don't support
core-js for IE11 in 2025 in a modern-only app. Remove.
23. Ignoring the Profiler
Production feels slow but nobody profiles. The React DevTools Profiler + DevTools Performance panel are free tools sitting unused.
24. Not testing on slow hardware
Dev hardware (M1/M2/M3 on fiber) hides perf issues that hit real users on mid-range Android.
Mental model
Most production React pain comes from a small list of patterns. Avoid them, and the app stays fast, secure, accessible, and maintainable. Catch them in code review and CI rather than discovering them at scale.
Follow-up questions
- •Why is React Query better than Redux for server data?
- •When does memoization actually help?
- •How do you set up CI perf budgets?
- •What error monitoring tool would you recommend?
Common mistakes
- •Server data in client state — duplicate sources of truth.
- •Index keys in dynamic lists — reorder bugs.
- •No error boundaries — one throw nukes the app.
- •Inline literals on memoized children — memo never fires.
- •Tokens in localStorage — XSS readable.
- •No CI perf budget — silent regressions.
Performance considerations
- •The cumulative cost of these mistakes is what makes production React 'feel slow.' Each is small; together they add up to laggy INP, slow LCP, and bloated bundles. Fixing them is mostly hygiene, not heroic optimization.
Edge cases
- •RSC changes some of these (no client state for server components).
- •Concurrent features (useTransition, Suspense) need careful adoption.
- •Strict Mode double-invocation is dev-only but should reveal real bugs.
- •Hydration mismatches from SSR + window/Date in render.
- •useId for stable SSR-safe ids.
Real-world examples
- •Most React codebase audits surface 5-10 of these in the first hour.
- •Sentry / Datadog / Honeybadger all integrate with React error boundaries.
- •React 19 Compiler will auto-fix some of these (memoization).