Skip to main content

Command Palette

Search for a command to run...

Why I Stopped Over-Engineering Components

I used to build every component like it needed to handle every possible future use case. It made the code worse, not better — here's what changed.

Updated
4 min readView as Markdown
Why I Stopped Over-Engineering Components
J
Jayesh Sojitra | AI & Frontend

Early in my career, "good code" meant flexible code — every component built with extra props, extra configuration, extra abstraction, anticipating requirements that hadn't shown up yet. That instinct felt responsible. In practice, it usually made things worse.

What over-engineering actually looked like, honestly

// A "flexible" button, built for requirements that didn't exist yet
function Button({
  variant = 'primary',
  size = 'medium',
  icon,
  iconPosition = 'left',
  loading,
  loadingText,
  fullWidth,
  rounded,
  elevation,
  onHoverColor,
  disabledTooltip,
  ...rest
}) {
  // 80 lines of conditional logic to handle every combination
}

This looks thorough. In practice: most of those props were never used in more than one place, the conditional logic to support all of them was genuinely hard to reason about, and every new requirement still somehow needed a prop that wasn't anticipated — because you can't actually predict future requirements by adding more configuration today.

The pattern I noticed, eventually

The components that caused the least pain over time weren't the most flexible ones — they were the simplest ones that solved the actual current problem clearly, and got refactored when a genuinely new requirement showed up. Abstraction added before a second real use case existed was, almost every time, guessing wrong about what that future flexibility would actually need to look like.

The rule that replaced my old instinct: wait for the third occurrence

  • First time you need something: just write it directly, inline, for that specific case. No abstraction yet.

  • Second time you need something similar: you now have two real examples — but resist abstracting yet, because two data points aren't enough to know the right shape of a general solution.

  • Third time: now you have enough real examples to see the actual pattern — what's genuinely shared, and what only looked shared but is actually different. Abstract now, based on real evidence, not speculation.

// First use: just build the specific thing
function SubmitButton() {
  return <button className="bg-blue-600 text-white px-4 py-2 rounded">Submit</button>;
}

// Third real, similar need shows up: NOW extract the actual shared pattern
function Button({ children, variant = 'primary' }) {
  const styles = {
    primary: 'bg-blue-600 text-white',
    secondary: 'bg-gray-200 text-gray-900',
  };
  return <button className={`${styles[variant]} px-4 py-2 rounded`}>{children}</button>;
}

Why "wait for the third occurrence" works better than anticipating needs

Speculative abstraction is a guess about the future, made with the least information you'll ever have about what that future actually needs. Waiting for real repetition means you're abstracting based on evidence — you can see exactly what varies and what doesn't, instead of guessing.

The honest cost of this approach

  • Some short-term duplication is expected, and that's fine — two similar-but-not-identical components existing briefly is cheaper than one wrong abstraction that everything gets awkwardly forced into.

  • This requires actually refactoring when the third case shows up — the approach fails if "wait for the third occurrence" quietly becomes "never revisit it," which does happen if refactoring isn't treated as a normal, expected part of the work.

Where more upfront structure genuinely IS worth it

  • A component you know for certain will be reused across many teams/apps (a genuine design system primitive) — different calculus than an app-specific component.

  • Requirements that are explicitly, concretely known in advance — not speculative "might need this later" but "the ticket says we need X and Y."

Try this yourself: look at the most over-configured component in your current codebase — the one with the most props, most conditional branches. Count how many of those props/branches are actually used in more than one place. That number is usually smaller than you'd expect, and it's the honest measure of whether that flexibility was earned or guessed at.

Takeaway: Flexible-sounding code isn't automatically good code — speculative abstraction built for hypothetical future requirements usually guesses wrong and adds real complexity cost today for a benefit that may never arrive. Building the specific thing first, and abstracting only once a real pattern has shown up three times, produces simpler code that's actually easier to change later, not harder.

30 Days of AI

Part 1 of 50

A 30-day series breaking down AI concepts, tools, and prompts in plain, jargon-free language — for beginners and professionals who want to actually understand and use AI, not just talk about it.

Up next

5 Frontend Security Mistakes That Are Easy to Miss

Security gets treated as a backend problem until a frontend mistake ships one of these five — all common, all easy to miss in a normal code review.