Refactor: When It Actually Changes Things

March 11, 2026By Emirhan YILDIRIM

Refactor: When It Actually Changes Things

When to Refactor (and When to Leave It Alone)

howtocenterdiv.com β€” "Software engineering is more than just centering a div."

Your part renders. Tests go well. The product is happy. Then, six months later, no one wants to touch that file. That's when refactoring becomes necessary.
But not every problematic file needs to be rewritten. The real talent is knowing when to refactor and when to leave things alone.

The Real Signal: Code That Fights Back

It's not about being clean when you refactor. It's about resistance. When a codebase starts to resist change, delivery slows down, defects multiply, and onboarding takes a week.
SignalWhat It MeansPriority
A bug fix needs changes in 4+ filesLogic is scatteredπŸ”΄ High
Adding a feature breaks unrelated testsCoupling is too tightπŸ”΄ High
The bundle size grew 40% without a featureDead code / bloated depsπŸ”΄ High
You copy-paste instead of reuseNo shared abstraction🟠 Medium
You need to read code twiceAbstraction is wrong🟠 Medium
New devs ask "why is this here?"Structure doesn't match intent🟑 Low
Do you see two or more of these signals? Refactoring gives a direct return on investment.

Before and After: Component Coupling

❌ Before β€” Tightly Coupled

tsx
1const UserCard = ({ userId }: { userId: string }) => { 2 const [user, setUser] = useState(null); 3 const [loading, setLoading] = useState(false); 4 const [error, setError] = useState(null); 5 6 useEffect(() => { 7 setLoading(true); 8 fetch(`/api/users/${userId}`) 9 .then(r => r.json()) 10 .then(data => { setUser(data); setLoading(false); }) 11 .catch(err => { setError(err.message); setLoading(false); }); 12 }, [userId]); 13 14 if (loading) return <div className="animate-pulse h-10 w-full bg-zinc-200 rounded" />; 15 if (error) return <div className="text-red-500 text-sm">{error}</div>; 16 17 return ( 18 <div className="flex items-center gap-3 p-4 border border-zinc-200 rounded-xl"> 19 <img src={user.avatar} className="w-10 h-10 rounded-full object-cover" /> 20 <div> 21 <p className="font-semibold text-zinc-900">{user.name}</p> 22 <p className="text-sm text-zinc-500">{user.email}</p> 23 </div> 24 </div> 25 ); 26};
You can't test the UI unless you simulate fetch. The card can't be reused. You can't change the skeleton without touching the business logic.

βœ… After β€” Separated Concerns

tsx
1// hooks/useUser.ts 2const useUser = (userId: string) => { 3 const [state, setState] = useState<{ 4 data: User | null; 5 loading: boolean; 6 error: string | null; 7 }>({ data: null, loading: false, error: null }); 8 9 useEffect(() => { 10 setState(s => ({ ...s, loading: true })); 11 fetch(`/api/users/${userId}`) 12 .then(r => r.json()) 13 .then(data => setState({ data, loading: false, error: null })) 14 .catch(err => setState({ data: null, loading: false, error: err.message })); 15 }, [userId]); 16 17 return state; 18}; 19 20// UserCardSkeleton.tsx β€” no dependencies 21const UserCardSkeleton = () => ( 22 <div className="flex items-center gap-3 p-4 border border-zinc-200 rounded-xl animate-pulse"> 23 <div className="w-10 h-10 rounded-full bg-zinc-200" /> 24 <div className="flex flex-col gap-1.5"> 25 <div className="h-3.5 w-28 rounded bg-zinc-200" /> 26 <div className="h-3 w-40 rounded bg-zinc-200" /> 27 </div> 28 </div> 29); 30 31// UserCard.tsx β€” orchestrator only 32const UserCard = ({ userId }: { userId: string }) => { 33 const { data, loading, error } = useUser(userId); 34 if (loading) return <UserCardSkeleton />; 35 if (error) return <p className="text-red-500 text-sm">{error}</p>; 36 if (!data) return null; 37 return <UserCardDisplay user={data} />; 38};
You may now test, render, and use UserCardDisplay anywhere. It is now a pure component.

CSS Refactoring: Specificity Wars

❌ Before

css
1.sidebar .nav-list .nav-item a { 2 color: #3b82f6; 3} 4 5.sidebar .nav-list .nav-item a:hover { 6 color: #1d4ed8; 7} 8 9.main-layout .sidebar .nav-list .nav-item a { 10 color: #6366f1 !important; 11}
The !important is a sign. Three layers of nesting that can't be changed in a predictable way.

βœ… After β€” Flat + Custom Properties

css
1:root { 2 --nav-link-color: #3b82f6; 3 --nav-link-hover: #1d4ed8; 4} 5 6.nav-link { 7 color: var(--nav-link-color); 8 transition: color 150ms ease; 9} 10 11.nav-link:hover { 12 color: var(--nav-link-hover); 13} 14 15[data-theme="purple"] { 16 --nav-link-color: #6366f1; 17 --nav-link-hover: #4f46e5; 18}
CSS Custom Properties β€” Browser Support:
BrowserMin VersionSupport
Chrome49+βœ… Full
Firefox31+βœ… Full
Safari9.1+βœ… Full
IE 11β€”βŒ None
Use postcss-custom-properties at build time for IE11. Ship natively for everyone else.

Refactoring for Performance

Layout Thrashing

tsx
1// ❌ Forces reflow on every iteration β€” 50 panels = 50 reflows 2panels.forEach(panel => { 3 const height = panel.getBoundingClientRect().height; // READ 4 panel.style.minHeight = `${height + 20}px`; // WRITE 5}); 6 7// βœ… One reflow total 8const heights = panels.map(p => p.getBoundingClientRect().height); // batch READ 9heights.forEach((h, i) => { panels[i].style.minHeight = `${h + 20}px`; }); // batch WRITE
This shows up in Chrome DevTools as a "Recalculate Style" block that goes away completely after the fix β€” saving 200–800ms.

Bundle Bloat

import _ from 'lodash';              // ❌ ~72KB gzipped
import sortBy from 'lodash/sortBy';  // βœ… ~2KB gzipped
Import StyleCostTree-shakeable
import _ from 'lodash'~72KB❌
import { sortBy } from 'lodash-es'~2KBβœ…
Native Array.prototype.sort0KBβœ…

When Refactoring Costs More Than It Saves

ConditionRefactor?
Code is touched 3+ times per monthβœ… Yes
Code is a single utility used once❌ No
You're mid-sprint on a deadline❌ No
It has no tests⚠️ Tests first
You're refactoring because it "looks bad"❌ No
A bug traced back here 3+ timesβœ… Yes

CSS @layer β€” End Specificity Wars for Good

If you want to mix a design system with CSS from another source, @layer is the best tool.
css
1@layer reset, base, components, utilities; 2 3@layer reset { 4 *, *::before, *::after { 5 box-sizing: border-box; 6 margin: 0; 7 } 8} 9 10@layer components { 11 .card { 12 background: white; 13 border: 1px solid #e4e4e7; 14 border-radius: 0.75rem; 15 padding: 1.5rem; 16 } 17} 18 19@layer utilities { 20 .mt-auto { margin-top: auto; } 21 .sr-only { 22 position: absolute; 23 width: 1px; 24 height: 1px; 25 overflow: hidden; 26 clip: rect(0,0,0,0); 27 } 28}
No matter how sophisticated the selector is, a utility inside @layer utilities has lower specificity than any style that isn't layered. Tailwind comes with its own @layer setup. This is the fix if you're using !important to fight it.
Browser Support β€” @layer:
BrowserMin VersionSupport
Chrome99+βœ… Full
Firefox97+βœ… Full
Safari15.4+βœ… Full
Edge99+βœ… Full
For earlier targets, use @csstools/postcss-cascade-layers as a polyfill.

Tailwind: The cn() Pattern

tsx
1// ❌ No conflict resolution β€” px-4 + px-6 both stay in the DOM 2className={`px-4 py-2 ${variant === 'primary' ? 'bg-blue-600' : 'bg-zinc-100'} ${className}`} 3 4// βœ… tailwind-merge fixes conflicts β€” last one wins 5import { clsx, type ClassValue } from 'clsx'; 6import { twMerge } from 'tailwind-merge'; 7 8const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs)); 9 10className={cn( 11 'px-4 py-2 rounded-lg font-medium text-sm', 12 variant === 'primary' && 'bg-blue-600 text-white', 13 variant === 'secondary' && 'bg-zinc-100 text-zinc-900', 14 disabled && 'opacity-50 cursor-not-allowed', 15 className 16)}

The Refactoring Checklist

  • Does a test suite exist? If not, write tests first.
  • Is this on the critical path? ← Talk to your team about it.
  • What does "done" mean? Define the goal: e.g. "decouple UserCard from fetch."
  • PR diff under 400 lines? β†’ Larger diffs can't be properly reviewed or reverted.
  • Will this change a metric? Use Lighthouse CI to check CLS, LCP, and INP before and after.