Skip to main content
Writing
·10 min read

React at scale: component architecture decisions that actually matter

After building UIs for MLOps platforms, AI products, and SaaS dashboards, these are the decisions that determine whether a frontend scales or collapses.

Most React architecture articles focus on the wrong things. File structure, folder naming, whether to use barrel exports — none of it matters much in the long run. What matters is the decisions that are expensive to undo: state ownership, component boundaries, and the seam between your UI and your data layer.

After working on several large React codebases — an MLOps platform, two AI products, and a feedback SaaS — here are the decisions I'd make again and the ones I'd revisit.

State ownership: the first decision

The hardest debugging sessions I've had in React were always state ownership problems. Some piece of state lived in the wrong component, so it was either passed through 4 layers of props or duplicated across siblings and got out of sync.

The rule I follow now: state lives as close to where it's used as possible, and no closer than where it needs to be shared.

This sounds obvious, but it has a specific implication for shared state management: I don't reach for Redux or Zustand because the app is "complex." I reach for them when I have state that's genuinely shared across components that don't share a parent.

The RTK Query layer

Server state is different from UI state, and it deserves a different tool. RTK Query handles all server-side data in every codebase I own now. The entity tag model forces you to think about data relationships, the cache invalidation pattern is explicit and auditable, and the optimistic update API is well-thought-out.

The alternative — useState + useEffect + fetch — works for one component. It becomes a maintenance problem at scale because every component reinvents the same loading/error/refetch logic.

Component boundaries: where to cut

The two failure modes I see most often:

Too coarse: One component that does data fetching, layout, business logic, and rendering. It's impossible to test, reuse, or reason about.

Too fine: Everything split into tiny components with prop-drilling chains 6 layers deep. You've distributed the complexity without eliminating it.

The heuristic I use: cut a component boundary when you want to change the rendering behavior independently of the logic. If the test or the design change would touch both, keep them together.

Compound components for complex UI

For UI that has multiple related parts that need to share state without prop drilling — tabs, accordions, data tables with sorting/filtering — the compound component pattern is worth the setup cost.

The API surface becomes <DataTable> + <DataTable.Header> + <DataTable.Body> instead of a single component with 15 props. It's easier to extend and much easier to compose.

The data layer boundary

The decision I've changed my mind on most is where to put data transformation logic.

Early in my career, I'd transform API responses inside components — a useMemo that maps a raw API shape into what the UI needs. This works until the transformation logic gets complex or needs to be shared, at which point it's in the wrong place.

Now I put transformation logic in RTK Query's transformResponse or in a dedicated selector layer. The component receives exactly the shape it needs, and the transformation is testable in isolation.

TypeScript: strict mode is non-negotiable

Every codebase I've inherited without strict TypeScript has the same class of bugs: optional chaining missing in places that could be undefined, any spreading through the type system, and prop types that don't match what's actually passed.

tsconfig.json with "strict": true from day one. The upfront cost is real; the later cost of adding it to an existing codebase is much higher.

What I've stopped arguing about

  • File structure: Feature-based vs. type-based doesn't matter. Consistency matters.
  • Default vs. named exports: Pick one per file and be consistent.
  • CSS-in-JS vs. utility classes: Tailwind wins on team velocity in my experience, but this is legitimately a preference question.
  • State management library: RTK for server state, Zustand for global UI state, useState for local state. The library choice matters less than the discipline of which state goes where.

The decisions that matter are architectural. The debates that consume the most energy are usually not.