Back to the blog
post.md

Reusable components: when to create them and when they are overkill

Use practical criteria to create reusable components without premature abstractions, confusing APIs, or variations that are difficult to maintain.

ComponentsFrontend architectureVueReactMaintainability

Useful reuse comes from shared meaning, shared changes, and a clear contract — not merely from code that looks similar.

Componentization is one of the first ideas we learn in frontend development. Instead of repeating interface and behavior, we create a unit with its own name, properties, and responsibilities. When that boundary works, an important change can happen in one place and remain consistent across the interface.

The problem starts when `reusable` becomes a goal before there is a real pattern to reuse. Two similar blocks appear, we extract a generic component, and soon it gains properties to hide a title, change the order, remove spacing, alter click behavior, and allow an exception that exists on only one screen.

There is less duplicated code, but the decision is no longer visible where it is used. To understand a simple screen, we now have to interpret a broad API and a chain of conditionals. That is why the question I use is not only `does this repeat?` It is `does this abstraction make meaning, change, and usage simpler?`

Reuse is not the same as removing all duplication

Duplication is a signal to investigate, not an automatic refactoring order. Two structures may have the same markup today while representing different concepts.

A project card and an article card, for example, may both start with an image, title, summary, and link. Their information hierarchy, states, actions, and responsive behavior may still evolve in different directions. Putting both inside a `UniversalCard` too early connects changes that may never belong together.

The opposite also happens. An element that appears only a few times may concentrate an important rule. A button used on a small number of screens may still deserve a component if it must guarantee the same loading, focus, disabled, icon, and hit-area behavior.

The most useful evidence is not a rigid count of two or three repetitions. It is whether the implementations share meaning and whether their changes need to keep happening together. This position is close to Kent C. Dodds’s AHA principle and Sandi Metz’s warning about the wrong abstraction.

What a component should simplify

A good component creates an understandable boundary. Its name should reveal its purpose, and its consumers should provide only the information that matters. Internally, it can handle repetitive details, states, and accessibility rules.

  • Meaning: turns a generic group of elements into a recognizable concept.
  • Change: concentrates adjustments that genuinely need to remain consistent.
  • Interaction: keeps keyboard, focus, loading, feedback, and disabled states aligned.
  • Usage: offers an API that is smaller and clearer than its internal implementation.
  • Composition: defines where content and real variations can fit without predicting every future screen.

If a component only moves code to another file while the screen still needs to know every internal detail, it may not have created a useful abstraction yet. This connects to John Ousterhout’s idea of deep modules: an interface should be considerably simpler than the functionality and complexity it hides.

An abstraction ladder

Instead of choosing between copying everything and building a design system, I prefer to think in four levels. Moving upward requires enough evidence to support a more widely shared contract.

1. Local implementation

The code stays on the screen while the problem is still being understood. A small amount of duplication can preserve context and allow each case to evolve without an artificial connection.

2. Local component

Once a block has a clear name and responsibility, it can be extracted to organize the screen even if it is still used only once. The goal here is readability and isolation, not global reuse.

3. Shared component

When the same concept appears in more than one context and changes start repeating, promoting the component becomes useful. Real cases now exist to guide properties, slots, composition, and tests.

4. Design-system primitive

Foundational elements such as buttons, fields, badges, and modals may gain broader visual, interaction, and accessibility rules. This level requires a stable contract, documentation, and deliberate maintenance because one change can affect many parts of the product.

Abstraction ladder with local implementation, local component, shared component, and design-system primitive.
An abstraction can start close to its use and move upward as shared meaning, repeated changes, and a stable contract emerge.

There is no requirement to reach the design-system level. An abstraction should stop where it continues to help.

Signals that extraction is worthwhile

  • The block represents the same concept in different places.
  • Fixes or adjustments must be repeated across the same implementations.
  • There is a small, known set of variations.
  • The component name communicates intent better than exposed markup.
  • Interaction or accessibility rules must remain consistent.
  • The public API can be smaller than the details it hides.
  • Current use cases define a contract without guessing future needs.

Consider a button that must block repeated clicks during an action, display loading without changing width, preserve visible focus, and support icons in known positions. Keeping those rules inside a `BaseButton` reduces more than duplicated lines: it reduces the number of different ways the same behavior can be implemented.

Usage examplevue
1<BaseButton variant="primary" :loading="saving">2  Save changes3</BaseButton>

The value comes from a recognizable contract. The screen provides intent, variant, and state; the component handles shared details.

Signals of premature abstraction

  • It was created for two blocks that only look similar.
  • Its name is so generic that it does not explain the concept.
  • Every new use adds a boolean or an escape-hatch property.
  • Property combinations produce states that are difficult to predict.
  • The screen must understand the internal structure to use the component.
  • Slots, callbacks, and properties merely forward everything to another element.
  • Changes from one context complicate unrelated contexts.
  • The shared implementation is harder to understand than the separate versions.

A universal component may look flexible, but the number of controls often reveals that different concepts were grouped by appearance.

An API that accumulated exceptionsvue
1<UniversalCard2  :clickable="true"3  :show-header="false"4  :compact="isMobile"5  :hide-footer="!hasActions"6  :reverse-media="featured"7  :remove-padding="customLayout"8  :use-special-border="campaignMode"9/>

More specific components such as `ArticleCard` and `ProjectCard` may duplicate part of the structure while producing a clearer architecture.

Real variations before imagined properties

Properties should represent decisions that already exist in the component’s domain. A button may have `primary`, `secondary`, and `ghost` variants because those roles consistently appear. Creating combinations for hypothetical screens expands the API without immediate value.

  1. Is it a legitimate variation of the same concept?
  2. Should it behave like the existing variations?
  3. Does the property name make the intent clear?
  4. Does its combination with other properties remain predictable?
  5. Would external composition or a specific component be simpler?

Escape hatches such as `noPadding`, `customClass`, `disableDefaultBehavior`, and `specialMode` are not always wrong. When they accumulate, however, they suggest that the original boundary may need to be reconsidered.

When to extract behavior instead of interface

Not every repetition belongs in a visual component. If two screens share data loading, transformation, or state control but present different results, the abstraction may be a function, a Vue composable, or a React hook.

Vue documentation recommends composables for reusing logic and components when logic and visual layout need to be reused together. React documentation takes a similar direction by presenting custom Hooks as a way to share logic through concrete, clearly named use cases.

It is also possible to share a smaller piece. Instead of a universal form, a `FormField` may own the label, description, error message, and accessible association while each screen composes the inputs it needs.

A practical decision flow

  1. Which concept am I naming? If the answer is only `a similar block`, the boundary is not clear yet.
  2. Which changes need to happen together? Change history reveals the real coupling.
  3. Which variations already exist? The API should emerge from concrete cases.
  4. Will usage become simpler? The call site should reveal intent instead of moving complexity.
  5. Does interaction need consistency? Focus, keyboard, feedback, and state rules may justify sharing early.
  6. Is the shared logic visual? A function, composable, or hook may be a better boundary.
  7. Can it start locally? A component can mature close to its first use.
  8. Can the decision be reversed? Splitting or removing an abstraction that stopped helping should remain possible.

A good abstraction removes repeated decisions. If it requires more context, exceptions, and combinations than the cases it replaced, it may be too early.

References and further reading

These sources do not form a single rule for componentization. They offer complementary perspectives on evidence-driven abstraction, module boundaries, incremental refactoring, component hierarchy, and logic reuse.

For a shorter starting path, I would read `The Wrong Abstraction`, continue with the documentation for the framework used in the project, and then move to `A Philosophy of Software Design`.

Limits and caveats

Different projects have different costs. In a small interface, controlled duplication may be cheaper than maintaining a shared layer. In a product with many teams, inconsistencies in foundational elements may justify components and documentation earlier.

Single-use components are not automatically wrong either. They can organize a large screen, isolate state, or name an important section. The mistake would be to call every extraction reuse or promote every local component into a shared library.

Similarly, an API with many properties is not necessarily inappropriate when the domain truly has many variations. The warning appears when those variations have no clear relationship, create invalid combinations, or exist only to preserve an old abstraction.

Conclusion

Reusable components help when they connect implementations that share meaning, change, and behavior. They get in the way when they connect only temporary similarities.

The safer path is usually incremental: implement clearly, observe changes, extract a local component, promote it when the pattern repeats, and create a global primitive only when its contract is stable.

The main lesson is that reuse cannot be measured only by how many lines disappeared. If the abstraction requires more context, exceptions, and combinations than the cases it replaced, keeping two simple implementations may be the more technical choice.