Component composition patterns in React that scale past 100 developers
The Architecture Paradox of Scaling Frontend Teams
When I first started building dashboards for our SaaS platform in Tokyo, we had three engineers. We could move fast, break things, and refactor our entire component library in an afternoon. Today, we have over 100 developers touching the same repository. The challenges have shifted from "how do I build this component?" to "how do I ensure this component doesn't break the entire application when someone on a different team touches it?"
Scaling frontend teams is not just about organizing folders or enforcing ESLint rules; it is about managing the mental model of the system. In the context of Next.js App Router and React Server Components (RSC), scaling is achieved through clear, architectural boundaries. We are moving away from the era of 'global state as a crutch' toward an era of 'data locality and composition.'
The Fallacy of the 'Monolithic' Component Library
Many organizations try to solve scaling by creating a massive, shared component library (UI Kit). While useful for consistent buttons and inputs, it often becomes a bottleneck. When 100+ developers rely on a single package for every atomic element, the "dependency hell" sets in. We found that teams began circumventing the library because the PR review process for a core library was too slow.
Instead, we transitioned to a design that prioritizes functional composition over static inheritance. We stopped asking, "How can I make this button more flexible?" and started asking, "How can I make this feature self-contained?" By leveraging RSCs, we shift the responsibility of orchestration to the server. The server determines the layout and the data fetching strategy, while the client focuses purely on interactivity. This removes the need for complex, prop-drilled configuration objects that plague legacy React codebases.
Data Locality and the RSC Advantage
In our current stack, every feature is a slice of the page defined by an async Server Component. The beauty of the App Router is that it encourages developers to keep data fetching as close to the UI as possible. Consider the following pattern for a high-traffic dashboard widget:
# A typical directory structure for an isolated RSC feature
feature-dashboard-widget:
- page.tsx # Entry point for the feature
- widget.tsx # Main Server Component (data fetching logic)
- actions.ts # Server Actions for mutations
- loading.tsx # Streaming UI skeleton boundary
- client-wrapper.tsx # 'use client' boundary for interaction
By encapsulating the data fetching logic within the widget.tsx (the RSC), we ensure that a failure or latency issue in this specific component does not block the rendering of the entire dashboard shell. We use Suspense boundaries to isolate these components. When 100 developers work across different product teams, they can own their specific feature folders. Because they own their own data fetching, they don't need to coordinate with the platform team to add a new API endpoint. They write their own fetch inside the RSC, define their own loading.tsx for streaming, and they are done.
Implementing Strict Boundary Boundaries
One of the most dangerous patterns in a large-scale React application is 'prop drilling' from the root layout down to a deep leaf node. When a state change at the top triggers a re-render of the entire tree, your performance dies and your architectural integrity vanishes.
We enforce a rule: No prop drilling beyond three levels. If a component needs data that exists at the top level, we refactor it into an RSC or use a React Context only if the state is truly global (like dark mode or authentication status). For feature-specific state, we rely on Server Actions and the useOptimistic hook. This keeps the data flow unidirectional and, more importantly, keeps the logic predictable.
// Example of a data-fetching RSC with streaming capability
async fun getWidgetData(userId: String): WidgetData {
// Simulate data latency
delay(1500)
return repository.fetchData(userId)
}
// React Server Component
// The component remains lean and streaming-ready
async fun DashboardWidget(userId: String) {
val data = getWidgetData(userId)
return "<div>{data.title}</div>"
}
By separating the 'data orchestrator' from the 'UI presenter', we make the code significantly easier to test. If a developer wants to test the data-fetching logic, they can write a unit test for the service layer without mounting a single React component. This is how you scale to 100 developers: by decoupling the UI from the underlying infrastructure.
Streaming UI: The User-Experience Multiplier
Streaming UI isn't just a technical trick; it's a social contract between engineering teams. In a large company, different teams have different backend performance profiles. Team A might have an API that returns in 50ms, while Team B’s legacy service takes 2 seconds.
In a traditional client-side rendering model, Team B's latency slows down Team A's UI. By utilizing Suspense boundaries and streaming SSR, we allow the page to render progressively. Team A’s component appears instantly. Team B’s component shows a loading state (which is designed by the product team to feel like part of the experience).
This architecture creates a 'fault-tolerant' frontend. If Team B’s service goes down, the rest of the application remains functional. We use error.tsx boundaries to capture errors at the component level. If a widget crashes, the user sees a small error message in that widget, while the rest of the dashboard remains usable. This pattern is essential for large-scale SaaS because it prevents a localized bug from resulting in a global outage.
Conclusion: The Path Forward
Scaling an engineering team beyond 100 people is an exercise in reducing cognitive load. Every time a developer has to wonder, "Where does this data come from?" or "Will this change break someone else's widget?", we have failed.
By adopting React Server Components, we gain three distinct advantages:
- Data Locality: Teams own their data fetching logic entirely within their feature directory.
- Performance Resilience: Streaming UI patterns prevent slow or failing services from blocking the critical path of the application.
- Compositional Clarity: By enforcing strict 'use client' boundaries and limiting prop drilling, we create a system where components are truly modular.
In our Tokyo office, we have seen that the most effective way to scale is not to impose more rules, but to build a better container. The App Router provides that container. It forces us to think about the server-client boundary explicitly. It encourages us to ship less JavaScript by default. And most importantly, it allows 100 developers to work on the same dashboard without stepping on each other's toes.
When we talk about the 'Next.js Way,' we are really talking about the 'Scalable Way.' We build shells that are lightweight, we fetch data as close to the UI as possible, and we stream the results. This approach turns the frontend into a collection of independently deployable, fault-tolerant units. The result is not just a faster dashboard; it is a developer experience that feels like it scales infinitely, regardless of how many people are contributing to the codebase. If you are struggling with team friction, stop looking at your state management library and start looking at how you are composing your components. If your architecture makes it easy to do the right thing, the team will naturally move in the right direction.