When the same logic appears in three or more places, extract it into a shared function, hook, or module. Duplicated code means that when you fix a bug or change behavior in one copy, the other copies silently remain broken — leading to inconsistent behavior, hard-to-trace bugs, and wasted time tracking down "why does it work here but not there."
Why This Matters
Duplicated logic creates consistency bugs — when you fix a bug or change behavior in one copy, the other copies remain broken. With 3+ copies, the probability of missing one during a change approaches certainty. Extraction at the right time reduces maintenance burden without premature abstraction.
The "Rule of Three" is a pragmatic guideline: tolerate duplication twice, extract on the third occurrence. This balances two competing risks:
Too early extraction — abstracting after the first duplication often produces the wrong abstraction because you don't yet understand the full pattern
Too late extraction — letting duplication grow past 3 copies makes it increasingly likely that copies will diverge, creating subtle bugs
The rule
When the same logic appears in 3 or more places, extract it to the appropriate layer:
Same component logic → custom hook or utility function
Same API pattern → shared service function
Same data transformation → shared mapper/formatter
Same validation → shared validation schema
Bad example
// In user-profile.tsxfunction formatUserName(user: User) { return user.name ? `${user.name} (@${user.username})` : `@${user.username}`;}// In comment-card.tsx — same logic, copy #2function formatAuthor(author: User) { return author.name ? `${author.name} (@${author.username})` : `@${author.username}`;}// In team-member-list.tsx — same logic, copy #3function getMemberDisplay(member: User) { return member.name ? `${member.name} (@${member.username})` : `@${member.username}`;}
Three copies with different function names but identical logic. When the format needs to change (e.g., adding verified badges), you must find and update all three.
Good example
// lib/utils/format-user.ts — single source of truthexport function formatUserDisplayName(user: { name?: string; username: string }) { return user.name ? `${user.name} (@${user.username})` : `@${user.username}`;}
// All three components import the shared functionimport { formatUserDisplayName } from "@/lib/utils/format-user";// user-profile.tsx<span>{formatUserDisplayName(user)}</span>// comment-card.tsx<span>{formatUserDisplayName(comment.author)}</span>// team-member-list.tsx<span>{formatUserDisplayName(member)}</span>
Where to extract
The extraction target depends on what the duplicated code does:
Duplicated code
Extract to
UI rendering pattern
Shared component
State + effect logic
Custom React hook
Data transformation
lib/utils/ function
API call pattern
lib/services/ function
Validation logic
Shared Zod/Yup schema
Database query
lib/repositories/ function
How to detect
Duplication detection is best done during code review. Look for:
Functions with different names but similar structure
Copy-pasted blocks with minor variable name changes
Similar try/catch patterns across multiple files
Repeated conditional chains with the same shape
Automated tools like jscpd can detect copy-paste patterns:
npx jscpd --min-lines 5 --min-tokens 50 src/
When NOT to extract
Only 2 copies exist — wait for the third to understand the pattern
The logic is coincidentally similar — same structure but different domain meaning
The abstraction would be more complex than the duplication — sometimes 3 simple copies are clearer than 1 generic function with 5 parameters