The boundaries I think keep a large React codebase workable

A magnolia bud unfurling in deep pink against a soft, sunlit background

A large React codebase does not stay workable by accident. Left alone it drifts toward one big folder where everything imports everything, state lives wherever it was first needed, and nobody can say who owns what. I have come to think that a few boundaries do most of the work of preventing that, and that the boundaries are as much about teams as about code.

A feature owns its state and exposes one surface

The first boundary is the feature folder: a directory that contains everything a feature needs, its components, its hooks, its state, its tests, and one index file that says what the rest of the application is allowed to use. Everything not exported from that file is private.

This does two things. It keeps a feature's internals from being depended on by accident, which is how a small refactor becomes a cross-codebase change. And it gives each feature an owner, because a folder with a public surface is a thing a team can be responsible for in a way that a scattering of files cannot.

Data loads at the edges

The second boundary is where data comes from. I think data fetching belongs at the edge of a feature, in a route or a top-level component, with the components below receiving what they need as props or from a nearby context. When leaves fetch their own data, the same request happens five times, loading states multiply, and a change to the data shape becomes a hunt through the tree.

Pushing it to the edge makes the data flow legible. Someone reading the route knows what the page needs. Someone reading a leaf component knows it is pure.

The component library lives apart

The third boundary is between the application and its component library. The buttons, inputs, dialogs, and layout primitives are a separate package, or at least a separate top-level directory with stricter rules: no application imports, no business logic, accessibility built in, versioned changes. Application code uses the library. The library never knows the application exists.

I think this matters because the two change at different speeds and for different reasons. A component library that is allowed to import from a feature stops being a library and becomes part of the feature.

Shared state has to earn its place

The fourth boundary is around global state. Most state in a React application is local: it belongs to a component or a feature and should stay there. State goes global when more than one feature genuinely needs it, and I think that should be a deliberate decision with a name attached, not the path of least resistance. A global store that anyone can add to becomes the big folder all over again, with the added problem that everything re-renders.

A linter enforces it

The last boundary is the one that makes the others real. Every rule above can be written as a lint rule: no imports across feature folders except through the index, no fetching below a certain depth, no application imports in the library, a review required for anything added to the global store. Rules that live in the linter are enforced on every change by nobody in particular, which is the only kind of enforcement that survives a busy quarter.

Boundaries are team boundaries

What ties these together is that each boundary is also a line on the org chart. A feature with a public surface can be owned. A component library that lives apart can have a team. Data that loads at the edge can be reviewed by the people who own the route. I think a large codebase stays workable when its structure matches how its people are organized, and the boundaries are how that match is kept.

Photo source: https://photos.robertstowe.com/flowers