Architecture

Scaling React Apps: Lessons from 10 Years in the Trenches

BBharath Bheemireddy9 min read

Hard-won lessons on component architecture, state management, and team-scale patterns that actually hold up in production.

I've been writing React since 2017. Across freelance projects, Colosseum Group, and now leading a team at Evoke Technologies, I've watched apps grow from a handful of components into systems with hundreds of routes and dozens of contributors. Some of those patterns aged like wine. Most aged like milk. Here's what actually held up.

1. Co-locate by feature, not by type

Early on I bought into the classic React folder structure: a top-level components/, hooks/, utils/, services/. It feels tidy on day one. By month six, you're hopping between five folders to change one feature. The fix is brutal in its simplicity — group files by the feature they belong to, not by what they technically are.

src/
  features/
    checkout/
      Checkout.tsx
      useCheckout.ts
      checkout.api.ts
      checkout.types.ts
    profile/
      Profile.tsx
      useProfile.ts
  shared/
    Button.tsx
    Input.tsx

Now a junior engineer can open one folder and understand a whole feature. Refactoring becomes safe — you delete a folder, you delete the feature.

2. State is a spectrum, not a single tool

The biggest scaling mistake I see is reaching for Redux (or now Zustand) for state that should live in a URL, a form, or React Query. Before you add a new store, classify what kind of state you have:

  • Server state (data from APIs) → React Query / SWR. It owns caching, refetch, retries.
  • URL state (filters, tabs, pagination) → search params. Bookmarkable, shareable, free.
  • Form state → React Hook Form or controlled inputs. Don't store typing keystrokes globally.
  • Local UI state (modal open, dropdown) → useState in the component that owns the UI.
  • Truly global app state (auth user, theme) → a small Zustand/Context store.

When I joined a team that had moved all server data into Redux, our cache logic alone was 1,200 lines of brittle code. Migrating to React Query deleted 80% of it and made the app feel snappier.

3. Components have boundaries — respect them

Three rules that prevent 90% of cross-component pain:

  • A component owns its layout and props — it does NOT reach into its children's internals.
  • Pass data down, send events up. Avoid props that drill more than 3 levels.
  • If a parent needs to know what a child is doing, hoist the state or use Context.

4. The 'one big page component' anti-pattern

Every Next.js codebase I've inherited has at least one 800-line page.tsx. It's not the file size that hurts — it's that the file owns layout, data fetching, business logic, and 12 modal states. Break it apart by what changes for what reason:

  • Page component → routing, layout, top-level data fetching only.
  • Feature components → own their state and effects, kept focused.
  • Hooks (useX) → wrap data and logic so components stay declarative.

5. Performance is mostly about not rendering

React DevTools profiler over the years taught me one thing: the most expensive render is the one you didn't need. Before reaching for useMemo or React.memo, look for the actual cause.

  • Is a context provider value identity changing every render? Wrap it in useMemo.
  • Is a parent re-rendering an expensive tree because of an unrelated state change? Split components.
  • Is a list re-rendering because keys are array indices? Use stable IDs.
Memoize the algorithm, not the symptom. If React.memo fixes your bug, you usually have a bigger structural issue waiting.

6. Types are a design tool, not a chore

TypeScript catches bugs, sure. But the bigger win is that types force you to design before you implement. Discriminated unions for component variants, generic hooks, and 'impossible state' types have probably saved my teams thousands of hours of bug-fixing.

type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error';   error: string }

// You literally cannot read `data` while status is 'loading'.
// The compiler enforces it.

Closing thought

Most 'scaling React' problems aren't React problems. They're team-scale problems wearing a React costume. Set conventions early, write them down in a CONTRIBUTING.md, and revisit them every quarter. Future you (and the developer joining the team in three months) will thank you.

B

Bharath Bheemireddy

Technical Lead @ Evoke Technologies

Get in touch