When to Extract a Component (and When to Just Copy-Paste)
The 3-usage-sites heuristic for component extraction, a premature ConfirmDialog that cost me time, and why copy-paste is sometimes the right call in Vue.
Early in building Polaris, I extracted a ConfirmDialog component after using it exactly once. I figured I'd need it everywhere (deletes, password changes, dangerous actions). Classic foresight.
Then the second use case needed a text input inside the dialog. The third needed two buttons with custom labels instead of "Confirm/Cancel". The fourth needed async validation before confirming. Within a month, my clean ConfirmDialog had 8 props, 3 slots, and a mode prop that changed how it behaved.
I'd have been better off copy-pasting the original 20 lines and tweaking each one independently.
The 3-Usage-Sites Rule
I now wait until three call sites would all benefit from the same change before extracting. Looking similar doesn't count.
Two similar components might diverge tomorrow. Three that stay identical for a few weeks probably share real structure worth abstracting.
The Price Display
The price display was worth extracting. In Polaris, prices show up in invoices, product lists, reports, and receipts. Every one of them needs the same formatting: locale-aware number formatting with the currency symbol from the shop's settings.
<script setup lang="ts">
import { computed } from 'vue'
import { useShopStore } from '@/stores/shop'
const props = defineProps<{
amount: number
}>()
const shop = useShopStore()
const formatted = computed(() =>
new Intl.NumberFormat(shop.locale, {
style: 'currency',
currency: shop.currency,
}).format(props.amount)
)
</script>
<template>
<span>{{ formatted }}</span>
</template>It has one prop and no slots or modes, and every call site wants the same thing: show this number as a price. When I changed the formatting logic to handle zero-decimal currencies, every price picked up the fix.
The test I use is whether a change here would fix a real inconsistency across the app. If so, extract. If each usage is going to diverge, copy-paste and let them evolve separately.
Signals That You Extracted Too Early
- The component has a
modeorvariantprop that changes its behavior, beyond styling. - You're adding slots to handle "special cases" in specific call sites.
- You find yourself passing callbacks through 3 layers of props to handle one caller's edge case.
- The component file is growing faster than the features that use it.
Signals That You Should Extract
- You fixed a bug in one place and realized you need to fix the identical bug in 2 other files.
- Three or more call sites pass the same props in the same pattern.
- A formatting rule (dates, currencies, statuses) should be consistent but you've implemented it slightly differently in each place.
The aim is to split code along the lines where it changes. Sometimes that means a shared component, and sometimes three similar-looking blocks that serve different purposes.