A platform team's design system ships connected components - an avatar that fetches the user from its id, a price tag that calls the pricing service from a sku, and a notification bell that opens its own WebSocket. Forty applications consume it. The package is 180 KB compressed and the bell is in the shared header on every page. Review this. What would you remove, what would you keep, and what would you leave alone even though it looks wrong?
Show the full answer Hide the answer
What is actually required
Forty teams need the same avatar to look the same, behave the same for a screen reader, and change once when the brand changes. None of that requires the component to know how to fetch a user. The data-fetching was added because every team wrote the same four lines, which is a real annoyance and the wrong thing to solve in this package.
What I would remove, and why it is safe to
- Fetching inside the avatar. A list of 24 comments renders 24 avatars, each firing its own request. The render tree has become the request graph, and nobody can see the waterfall in the network tab without knowing the component tree. Worse, the design system now owns a network contract — endpoint path, auth header, retry policy, error shape. A backend team renaming a field breaks forty applications at once, through a package whose version policy governs props and says nothing about HTTP.
- The WebSocket in the bell. It is in the header, so every page holds a connection whether or not anyone looks at notifications, and the connection count is now the page count. At roughly 30-60 KB of kernel and user-space buffers per idle connection on the server side, a million sessions is 30-60 GB of memory bought by a visual element.
- The pricing call. Price is the one value in a commerce interface that must not be stale or approximate, and it is now cached by whatever data layer happens to be in the host application.
What replaces them: ship the query, not the fetch. The design system exports a query key and a response type; the consuming application runs it through its own data layer, which already does deduplication, caching and retries once per page rather than once per component.
The one change that matters
Split the package. @ds/components stays presentational and has no network dependency. @ds/data is
optional, versioned separately, and may break more often, because it tracks services rather than design. That
single boundary turns one blast radius into two with different change rates.
What I would leave alone
The 180 KB. It is large, and a design system is loaded once and cached across routes, so its cost is amortised in a way a per-route bundle's is not. Attack it after the fetching is out, with tree-shaking evidence rather than instinct.
When not to do this
A single-team product with one application should keep connected components: the coupling the split prevents does not exist, and the boilerplate it removes is real. An embeddable widget shipped to third-party sites must also stay self-contained, because there is no host data layer to hand the query to.
Common weak answers
- "Make the fetching optional with a prop." Both paths now exist in forty applications and in the component's tests, and the network contract is still in this package.
- "Add React Suspense." It makes the waterfall render nicely. It is still a waterfall.