When Reusable Components Become the Wrong Abstraction

Two interfaces may look almost identical while behaving very differently. A practical look at when to share frontend components, when to split them, and how to keep the useful reuse.

On this page

Product listing and shopping cart interfaces connected to shared visual building blocks, highlighting their different actions and responsibilities.

Two interfaces can display a product image, name, and price without representing the same job. In a product listing, the interface helps someone compare options and decide what to explore. In a shopping cart, a similar-looking block represents an existing line item whose quantity, availability, and totals may need to stay synchronized with the commerce system.

Extracting both into a reusable ProductCard sounds efficient. Initially, it may be. The difficulty appears when changes meant for one context begin to complicate the other: listing promotions, cart quantity updates, different loading states, error recovery, and restrictions that do not belong to both.

Working with established e-commerce frontends has made me cautious about treating visual similarity as proof of a shared responsibility. The component example in this article is illustrative; it is not a client’s implementation, a claimed production incident, or a refactor I am presenting as completed work. The broader question is how to decide whether an abstraction still reflects the product.

Visual repetition is not the same as shared responsibility

A useful component has a recognizable responsibility and a dependable contract with its consumers. Repeated markup can indicate an opportunity to reuse something, but it does not tell us how much to reuse.

Consider three places where product information appears. Their similarities are real, but the surrounding decisions and state may not be:

SurfacePrimary responsibilityState and behavior
Product listingHelp shoppers discover and compare productsCatalog data, navigation, optional add-to-cart action
Cart lineRepresent and edit an existing order choiceServer-accepted quantity, totals, validation and rejection
RecommendationPresent relevant products in contextMerchandising rules, placement and discovery actions

The listing and recommendation contexts may still share a meaningful product-tile component if their interaction contract and data needs are sufficiently alike. The cart line is a more obvious place to investigate a separate boundary because it participates in mutations and confirmed cart state. Those are hypotheses to validate against the actual application, not automatic rules based on page names.

A small image wrapper, product-title link, price-formatting utility, or spacing token can be genuinely reusable across all three. The fact that these pieces are shared does not require the components coordinating their workflows to be shared as well.

The warning sign is an API that explains exceptions

A shared component often starts with a compact API: product data, display options, perhaps an action. Later requirements add context flags. None of these flags is necessarily a problem on its own. The design starts becoming difficult when the flags describe mutually dependent rules rather than independent visual variations.

Consider this hypothetical JSX usage. It is an API sketch, not production code or a complete implementation:

<ProductCard

mode=“cart”

compact

showQuantityEditor

hideAddToCart

priceMode=“confirmed”

onQuantityChange={updateLine}

/>

The component now needs to know that cart mode may require a quantity editor, should not offer a normal add-to-cart action, and must handle prices differently from the catalog view. What should happen if a caller sets mode to cart but leaves hideAddToCart false? What if the callback is missing? Are these legal combinations, impossible ones, or states the component silently tries to fix?

TypeScript can encode some of these constraints—for example, with a discriminated union so different modes require appropriate props. That improves API correctness. It does not answer whether these contexts should share a component in the first place. A perfectly typed abstraction can still combine unrelated responsibilities.

Sandi Metz described the broader problem in The Wrong Abstraction: a shared abstraction accumulates parameters and conditional paths when new requirements almost—but not quite—fit. The frontend-specific question is what the conditions are revealing about the user journeys and state boundaries.

Follow who owns the state and the consequences of changing it

The most useful distinction in the listing-versus-cart example may be state ownership rather than appearance.

A listing can often render from product or catalog data. An add-to-cart action is a separate interaction whose result can be acknowledged or rejected. A cart line, by contrast, must represent the cart’s current accepted state. Quantity changes can trigger validation, new totals, stock checks, or a server rejection. An optimistic display may need to reconcile with the authoritative response.

If one reusable component becomes responsible for catalog presentation, cart mutation, error rollback, promotional eligibility, and navigation, it is no longer merely coordinating shared UI. It is importing separate business concerns into one public API.

React’s guidance on sharing state between components emphasizes identifying a single source of truth for each piece of state. The principle applies beyond React: ask which part of the system owns the value, who is permitted to change it, and what happens if an attempted change fails. If the answers differ substantially between consumers, the component boundary deserves scrutiny.

There is also an ownership problem for teams. When work on cart rules repeatedly requires changes in a listing component’s shared internals, reviewers from both areas may need to understand conditional paths unrelated to their feature. Reuse has then reduced line duplication while increasing decision coupling.

Ask which future changes should move together

A useful architecture review is not “How many props does this component have?” It is “Which changes should affect every consumer of this component?” The answer is visible in recent and upcoming work.

Imagine two requests. Merchandising wants a promotional label on product-listing tiles. Separately, checkout needs to show a server-confirmed price adjustment on cart lines. If one ProductCard change must understand both requests and safeguard unrelated modes, the shared implementation may be connecting responsibilities that evolve independently.

Compare that with a request to improve how product images preserve their aspect ratio or how currency values are formatted. Those changes might reasonably affect both interfaces. The distinction is not between shared and duplicated pixels; it is between shared rules and unrelated rules.

Here is a practical set of signals to investigate before deciding to keep or split a component:

Signal to investigateQuestion that mattersPossible response
Props with dependent meaningsCan callers construct invalid combinations?Constrain the API; split if behavior differs
Different state ownersWho confirms a change and handles failure?Separate workflow logic by state ownership
Unrelated feature changesMust one consumer guard against another's change?Split if this coupling is recurring
Stable shared behaviorDoes the same rule apply to all consumers?Keep a shared primitive or utility
Presentation-only differencesDoes the underlying interaction remain the same?Consider an explicit, narrow variant
Independent evolutionWill these consumers usually change for different reasons?Prefer separate high-level components

No single row proves the abstraction is wrong. A few well-defined variants may be cheaper and clearer than separate components. Conversely, a low prop count does not make a shared component sound if it owns unrelated async state and business rules. Evaluate the pattern of change rather than one numeric threshold.

Separate workflows while keeping the useful reuse

If the evidence points to distinct responsibilities, a more coherent public interface might expose a listing tile and a cart line separately:

These are illustrative component boundaries, not a complete implementation. A listing tile can own discovery actions and merchandising presentation; a cart line can own cart-related interaction and confirmation states. Both may compose smaller shared elements for product identity, imagery, text patterns, accessible controls, or price formatting where the underlying behavior is truly the same.

Separation does not mean copying every detail into two independent systems. It means choosing the level at which reuse remains stable. Share the part that has one reason to change. Keep the different reasons to change near the consumers that actually need them.

An alternative is to retain one component with an explicit and type-safe variant API. That is reasonable when behavior is truly unified and differences are mainly presentation. The engineering decision is the boundary that reduces the work required for the next likely change—not a universal rule to create more or fewer components.

Undoing a poor abstraction without trading one problem for another

When an abstraction no longer fits, its previous value should not make the team afraid to revisit it. But a split is still an architectural change that needs a clear scope.

First, identify each consumer and describe what it relies on: rendered structure, interaction behavior, state ownership, and any accessibility requirements. Next, separate context-specific behavior without simultaneously redesigning every page. Extract only those small shared pieces whose contract is already understood. Move consumers one at a time and check their observable behavior. Finally, remove dead flags and conditional paths once their users are gone.

It can be useful to tolerate some duplicated markup during this transition. That duplication may reveal which parts are actually stable across the interfaces and which were only superficially similar. Re-extracting everything immediately can recreate the same mistaken boundary under new names.

The cost of separation should remain visible. Two components require their own tests and can drift in style or accessibility. Shared tokens, primitives, and review standards help prevent unnecessary divergence. The point is not to turn one maintenance problem into two.

How I would make the decision

For a real component under review, I would look at two recent changes and two plausible upcoming changes. For each one, I would ask which consumers ought to change, which consumers must not change, and what logic currently forces them to be reviewed together.

If the answers consistently reveal separate state owners, separate failure modes, and unrelated reasons to change, I would favor splitting the workflow components while keeping genuinely common primitives. If the consumers share the same interaction contract and usually evolve together, I would favor preserving reuse and making the variant API explicit.

Before proceeding, I would also test the alternative that requires less architecture: can a small change in composition, state placement, or type constraints solve the actual problem? Not every awkward prop calls for a refactor. Refactoring is worthwhile when it makes future changes safer or easier to reason about, not when it merely produces prettier component names.

The limits of interpreting reuse metrics are examined in What My Component-Driven Research Can—and Cannot—Show, which distinguishes a useful measurement from a general design rule.

The responsibility matters more than the component count

Reusable components are valuable because they make shared behavior consistent and reduce repeated work. They become counterproductive when a common wrapper hides workflows that no longer belong together.

The better question is not whether two screens look alike, nor whether their implementations share enough lines of code. It is whether they have a meaningful responsibility that should continue to evolve as one unit.

When that answer is yes, reuse protects consistency. When it is no, separating the responsibilities can be the more maintainable form of reuse: share what is stable, and stop forcing the rest to travel together.