How would you design a scalable React application for a dashboard with more than one hundred pages?
Feature-folder structure (not type-based), route-level code splitting (one chunk per page), a design system shared across pages, server-state in React Query (per-page caching), client-state minimized and scoped, lazy-loaded heavy widgets, a strict module boundary (pages can't reach into siblings), CODEOWNERS per area, and bundle budgets enforced in CI. The hard part is organizational, not technical.
100 pages means many teams shipping in parallel. Architecture is mostly about boundaries.
Folder structure: feature-based
src/
app/ # routing, providers, error boundaries
features/
billing/
pages/ # route components
components/ # local to billing
hooks/ # local data hooks
api/ # billing-specific server adapters
types.ts
index.ts # PUBLIC api — only this is importable from outside
users/
reports/
...
shared/
ui/ # design system
hooks/ # cross-cutting hooks
lib/ # utilitiesRules:
- Pages live inside their feature, not in a flat
pages/dir. - Cross-feature imports go through
features/X/index.tsonly. shared/has no app logic — only generic UI + utilities.
Enforce with ESLint no-restricted-imports or eslint-plugin-boundaries.
Routing + code splitting
const Billing = lazy(() => import('@/features/billing'));
const Users = lazy(() => import('@/features/users'));
<Routes>
<Route path="/billing/*" element={<Billing />} />
<Route path="/users/*" element={<Users />} />
</Routes>One chunk per top-level feature. Pages within a feature can split further if heavy.
Server state: React Query
- One
QueryClientfor the app. - Per-resource query keys:
['user', id],['invoices', { status }]. - Per-feature hook layer (
useUser,useInvoices) so pages never call fetch directly. - Cache lifetime tuned per resource (catalog: 1 hour, user balance: 30s).
Client state: minimized
Most 'global' state is actually server state. What remains:
- Auth/session — Context.
- Theme/locale — Context.
- Per-feature local state — useState/useReducer inside the feature.
If you reach for Redux/Zustand for everything, you're probably mismodeling server state.
Design system
A shared component library (shared/ui) is non-negotiable for visual consistency. Either:
- Build in-house on top of Radix/Headless UI + Tailwind.
- Adopt shadcn/ui and own the copy.
100 pages means 100 chances to drift; the design system is the lever that keeps them aligned.
Performance budgets
- Initial bundle: < 200 KB gzipped.
- Per-feature chunk: < 100 KB gzipped.
- Largest Contentful Paint: < 2.5s p75.
Enforce in CI with size-limit or webpack stats checks. Block merges that blow the budget.
Cross-cutting concerns
- Auth/permissions: a single wrapper that pages opt into; routes declare required roles.
- Telemetry: route changes auto-log; React Query mutations auto-log success/error.
- Error boundaries: one at app root for white-screens, one per page for graceful degradation.
- i18n: keys live with features; shared dictionary loaded lazily.
Organizational
- CODEOWNERS mirrored to feature folders.
- Architecture tests:
madgeor boundaries rules in CI to catch illegal imports. - Visual regression: Chromatic/Percy on the design system.
- Performance regression: Lighthouse CI per PR on key routes.
Where it breaks down without discipline
- 'Just one shared util' that grows into a god-folder.
- A 'common' components folder full of feature-specific widgets.
- Pages importing from each other's internals.
- Server state duplicated across slices.
Migration path if you're already deep
Don't rewrite. Pick the next page you ship, build it in the new structure, copy patterns from there for the page after. After ~10 pages the new structure dominates.
Follow-up questions
- •How do you enforce module boundaries in a large React app?
- •Where does authentication state live in this architecture?
- •How do you handle a heavy widget shared by 20 pages?
Common mistakes
- •Type-based folders (`components/`, `hooks/`, `utils/`) at the top — doesn't scale past ~15 pages.
- •Sharing through a god-utils file instead of a real shared module.
- •Pages reaching into each other's internals because there's no enforced boundary.
Performance considerations
- •Bundle splitting is the biggest lever. Per-feature chunks mean a billing user never downloads the reporting code. Preload high-traffic routes on idle. Image and font budgets matter as much as JS.
Edge cases
- •Cyclical dependencies between features — usually a sign you've conflated two domains.
- •Pages that span features (a dashboard combining billing + users) — model as a 'composition' feature.
- •100 pages but 80% rarely visited — code split aggressively, preload top-20.
Real-world examples
- •Shopify admin, Stripe Dashboard, Linear, GitHub, Notion. All publicly document feature-folder structures and route-level splitting. Linear in particular has written about strict module boundaries and the tooling that enforces them.