| name | react-refactor |
| description | Use when refactoring React components, cleaning up legacy frontends, optimizing performance, improving component architecture, or reducing bundle size |
React Refactoring & Performance
Overview
Safe component refactoring, performance optimization, and bundle size reduction for React/Vite/Bun frontends.
Core principle: Write characterization tests before refactoring. Profile before optimizing.
3-strikes rule: If the same refactoring approach fails 3 times, stop. The problem is architectural — escalate to system-design for a deeper review instead of thrashing.
Safe Refactoring Process
- Write characterization tests — capture current rendering and behavior
- Refactor — change structure, not user-visible behavior
- Verify — all tests pass, visual regression check
- Profile — only optimize what's measurably slow
Component Decomposition
Signals to split:
- Component > 200 lines
- Multiple responsibilities in one component
- Deeply nested JSX (> 4 levels)
- Props drilling through 3+ levels
Extract pattern:
function UserDashboard() {
}
function UserDashboard() {
return (
<div>
<UserHeader user={user} />
<UserStats stats={stats} />
<UserActivity activities={activities} />
</div>
)
}
Extract Custom Hook
function UserList() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => { fetchUsers().then(setUsers).finally(() => setLoading(false)) }, [])
}
function useUsers() {
return useQuery({ queryKey: ['users'], queryFn: fetchUsers })
}
function UserList() {
const { data: users, isLoading } = useUsers()
}
Performance Patterns
Profile first:
React DevTools → Profiler → Record → Interact → Analyze
Only optimize when profiling shows a problem:
| Issue | Solution |
|---|
| Unnecessary re-renders | React.memo on expensive children |
| Expensive computation | useMemo with correct deps |
| Callback identity | useCallback when passed to memoized children |
| Long lists | Virtualization (@tanstack/react-virtual) |
| Large bundle | Code splitting with React.lazy |
Remove unnecessary optimizations:
const fullName = `${first} ${last}`
const handleClick = () => { ... }
Bundle Optimization
bunx vite-bundle-visualizer
const Users = lazy(() => import('./features/users'))
import { format } from 'date-fns' // not import * as dateFns
const { Chart } = await import('chart.js')
State Management Refactoring
| Smell | Fix |
|---|
| Prop drilling 3+ levels | Context or state library |
| Context re-renders everything | Split contexts by update frequency |
| Global state for server data | React Query |
| URL state in useState | Use URL params (useSearchParams) |
Legacy Frontend Rescue
When inheriting a messy React codebase:
Step 1: Characterize Before Touching
test('UserList renders current behavior', async () => {
render(<UserList />)
expect(screen.getByRole('table')).toBeInTheDocument()
expect(await screen.findAllByRole('row')).toHaveLength(11)
})
Step 2: Identify the Worst Offenders
- No error boundaries — one component crash takes down the whole app
- Fetch in useEffect with no cleanup — race conditions, memory leaks
- Giant monolith components (500+ lines) — impossible to test, split first
- Class components with lifecycle spaghetti — migrate to hooks incrementally
- Inline styles everywhere — extract to consistent styling approach
- No loading/error states — blank screens on failure
- Direct DOM manipulation —
document.getElementById in React code
Step 3: Triage and Prioritize
find src -name "*.tsx" -exec wc -l {} \; | sort -rn | head -20
for f in $(find src -name "*.tsx" ! -name "*.test.*" ! -name "*.spec.*"); do
test_file="${f%.tsx}.test.tsx"
[ ! -f "$test_file" ] && echo "NO TEST: $f"
done
bunx ts-prune
grep -rn "document\.\(getElementById\|querySelector\|createElement\)" src/
Step 4: Incremental Migration
Don't rewrite — wrap and replace:
function SafeUserList() {
return (
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<Spinner />}>
<LegacyUserList />
</Suspense>
</ErrorBoundary>
)
}
function useUsers() {
return useQuery({ queryKey: ['users'], queryFn: fetchUsers })
}
function UserList() {
const { data: users, isLoading, error } = useUsers()
if (isLoading) return <Spinner />
if (error) return <ErrorMessage error={error} />
if (!users?.length) return <EmptyState />
return <UserTable users={users} />
}
Step 5: Add Missing States
Every data-fetching component needs all three:
if (isLoading) return <Skeleton />
if (error) return <ErrorAlert message={error.message} />
if (!data?.length) return <EmptyState message="No users found" />
return <UserTable users={data} />
Step 6: Remove Dead Code
bunx ts-prune
bunx knip
Delete it. Git has history.
Chains
- REQUIRED: Write characterization tests before refactoring — no exceptions
- REQUIRED: Update CLAUDE.md with discovered gotchas and conventions (
claude-md)
- Use
superpowers:systematic-debugging for performance investigation
- Legacy codebases: Run
fullstack-healthcheck first to prioritize what to fix