Front-end codebases rarely collapse from a single bad decision. They erode — a component that fetches its own data here, a shared piece of global state there, a utils folder that becomes a junk drawer. Six months later a small change touches twelve files and nobody is sure why. Scaling a React app is mostly about defending boundaries before they blur.
Decide where data is fetched, and hold the line
The single biggest source of front-end mess is components that each fetch their own data on mount, producing waterfalls, duplicate requests and loading spinners three levels deep. Fetch on the server where you can, colocate data requirements with the route that needs them, and pass data down as props. In the Next.js App Router, keep components as server components by default and drop to client components only where interactivity genuinely requires it.
- Fetch data at the route or server boundary, not scattered across leaf components.
- Keep the client bundle lean by defaulting to server components.
- Use a real data-fetching layer for client state so caching and revalidation are not hand-rolled.
Separate server state from UI state
Most bugs blamed on 'state management' come from treating server data — which is cached, shared and can go stale — the same as local UI state like whether a menu is open. Keep them apart: a query cache owns server data with its own invalidation rules, and lightweight local state owns the ephemeral interface. Reaching for a global store to hold data that came from an API is usually a sign the two got conflated.
Organise by feature, and make the shared layer earn its place
Group code by the feature it serves rather than by technical type, so a change to billing lives in one folder instead of scattered across components, hooks and utils directories. Promote something to the shared layer only when two features genuinely use it, and give shared UI a real component API. A design system of well-bounded components is what lets fifteen people work without stepping on each other.
- Colocate a feature's components, hooks and types under one folder.
- Promote to shared only on the second real use, not in anticipation of one.
- Give shared components explicit props and no hidden dependencies on app state.