UI Refactor Checklist
A sequenced walkthrough for inspecting React and Next.js UI files and deciding what to refactor. Run the steps in order across the files in scope (a route, a feature folder, or a PR diff). Earlier steps reshape the surface area; later steps tighten what remains. Skip a step only if you have already verified it is clean.
How to Use This Checklist
- Open every file in the feature or route at once --
app/<route>/page.tsx, its child components, itsactions.ts, and any colocated hooks. - Make one small PR per step that fires. Do not bundle a Server Component conversion with a memoization pass.
- If a step does not match, write a one-line note ("no
anyfound") so reviewers know you looked. - Stop after the first 3-5 high-impact steps if you are time-boxed -- the list is ordered by leverage.
Pass 1: Orient
Get a map of the files before changing anything. The first pass is read-only.
1. Map the route, then the boundary
Look for: Which file is the route entry (page.tsx, layout.tsx)? Which children are Server vs. Client Components? Where does the first "use client" appear?
Refactor when: "use client" sits at the top of a wrapper that renders mostly static content. Push the directive down to the smallest interactive leaf. Every kilobyte of static markup above the boundary is shipped as JS unnecessarily.
// before: whole page is client
"use client";
export default function Page() {
return <Layout><Header /><Sidebar /><InteractiveChart /></Layout>;
}
// after: only the chart is client
export default function Page() {
return <Layout><Header /><Sidebar /><InteractiveChart /></Layout>;
}
// InteractiveChart.tsx -> "use client";2. List each component's single responsibility
Look for: What does each file do in one sentence? If you cannot finish the sentence without "and" or "also," it is doing too much.
Refactor when: A component renders, fetches, validates, formats, AND tracks analytics. Split into focused pieces: a Server Component for data, a Client Component for interaction, utilities for formatting.
3. Trace the data flow
Look for: Where does each piece of data enter the tree? Props, context, server fetch, route param, URL search param, store?
Refactor when: The same value is fetched in two places, or props are passed through 3+ components untouched (prop drilling). Move the fetch to a Server Component, or lift state to a colocated provider, or compose with children instead of drilling.
Pass 2: Structure
Reshape the file boundaries. Do this before tightening types or memoizing -- moving code first makes those passes smaller.
4. Break up god components (>200 lines or >5 responsibilities)
Look for: A single file with mixed concerns: header markup, modal state, fetch logic, form, table, footer.
Refactor: Extract sub-components named by their role (OrderSummary, OrderItemList, OrderActions). Keep each under ~150 lines.
5. Replace prop drilling with composition or context
Look for: A prop passed through 3+ layers without being read. Or a component accepting 10+ props that are forwarded.
Refactor: Pass children so the parent supplies the leaf directly, or wrap the subtree in a context provider scoped to that feature.
// before: drilling user 3 levels deep
<Page user={user}><Layout user={user}><Sidebar user={user} /></Layout></Page>
// after: composition
<Page><Layout><Sidebar user={user} /></Layout></Page>6. Move data fetching out of useEffect
Look for: useEffect(() => { fetch(...).then(setData) }, []) in a component that does not need the browser.
Refactor: Convert the parent to an async Server Component and await the data, or use SWR / React Query for client-driven cases. useEffect for fetching is almost always wrong in App Router code.
7. Convert inline forms to Server Actions
Look for: <form onSubmit={...}> with fetch('/api/...') inside the handler.
Refactor: Define a Server Action in actions.ts, bind it via <form action={submit}>, and use useActionState for pending and error state. Free progressive enhancement and no client validation drift.
8. Lift duplicated JSX into a shared component
Look for: The same 8-15 lines of markup repeated across 2-3 files (card layout, empty state, error block).
Refactor: Extract to a shared component in the same feature folder. Do not over-abstract -- if it is used twice with different intent, leave it.
Pass 3: Types and Contracts
Once the shape is right, lock the contracts.
9. Eliminate any (and implicit any)
Look for: : any, as any, untyped function parameters, untyped useState().
Refactor: Replace with unknown at boundaries plus a type guard, or with the exact type. useState<Foo | null>(null) instead of useState().
10. Replace optional-prop soup with discriminated unions
Look for: Props like { loading?: boolean; error?: Error; data?: T } where only one is set at a time.
Refactor: Model as { status: 'loading' } | { status: 'error'; error } | { status: 'success'; data }. Impossible states become impossible.
11. Extract magic values to named constants
Look for: Bare numbers (< 3, 300, '#FF0000') and bare strings ('admin', 'pending') sprinkled in JSX or handlers.
Refactor: Hoist to a const at the top of the file or a shared constants.ts. Names beat comments.
12. Replace enum with as const unions
Look for: enum Status { ... } or string-keyed objects pretending to be enums.
Refactor: const STATUS = ['idle','loading','success'] as const; type Status = typeof STATUS[number];. Smaller bundle, structural typing, no runtime object.
Pass 4: Render Correctness
Catch the bugs that ship silently.
13. Fix key={index} on dynamic lists
Look for: items.map((item, i) => <Row key={i} ... />) where the list reorders, filters, or items are added/removed mid-list.
Refactor: Use a stable id (item.id). If no id exists, derive a stable hash from content.
14. Remove derived state stored in useState
Look for: const [fullName, setFullName] = useState($ $) plus an effect that re-syncs it.
Refactor: Compute it inline: const fullName = $ $``. State should be irreducible inputs, not their projections.
15. Audit useEffect dependency arrays
Look for: Missing deps (lint warning), or unstable deps (object/function literals recreated each render driving infinite loops).
Refactor: Memoize the unstable dep with useMemo/useCallback, OR move the value out of render, OR collapse the effect into an event handler. If the effect is just syncing a value, see step 14.
16. Add or right-size memoization
Look for: Heavy children re-rendering on every parent render (charts, tables). Or useMemo wrapping a + b.
Refactor: Wrap the heavy child in React.memo and stabilize its props. Remove memoization from anything cheap -- useMemo itself is not free.
Pass 5: Polish
Last pass. These are the small wins that make a feature feel finished.
17. Replace nested ternaries with early returns or a state object
Look for: cond1 ? a : cond2 ? b : cond3 ? c : d in JSX.
Refactor: Switch on a discriminated status field, or use early returns: if (loading) return <Spinner />; if (error) return <Error />; return <Data />.
18. Add Suspense and Error Boundaries at the right grain
Look for: A whole route falling back to one spinner, or one uncaught throw white-screening the page.
Refactor: Add loading.tsx per route segment for streaming. Wrap risky client subtrees in <ErrorBoundary> (or use Next.js error.tsx) so the rest of the page survives.
19. Remove imperative DOM ops in favor of declarative state
Look for: document.getElementById, el.classList.add, manual focus/scroll calls scattered in handlers.
Refactor: Drive the behavior from state and let React render it. Use ref + useEffect only for genuine imperative needs (focus management, third-party libs, measuring layout).
20. Audit accessibility and the visible/invisible labels
Look for: Buttons without text or aria-label, icon-only controls, missing <label htmlFor>, color-only state, missing alt on <Image>.
Refactor: Every interactive element needs an accessible name. Every image needs alt (empty string for decorative). Every form input needs an associated label. This is the cheapest UX improvement on the list.
When You Are Done
- Each step you fired produced one small, reviewable PR.
- Each step you skipped has a one-line note in the PR description ("step 12: no enums in this feature").
- Run the build, the test suite, and click through the feature in the browser before marking the audit complete.
- If you find a 21st pattern that recurs across the codebase, propose adding it to this list -- the checklist should grow with the codebase.