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.
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 clientexport default function Page() { return <Layout><Header /><Sidebar /><InteractiveChart /></Layout>;}// InteractiveChart.tsx -> "use client";
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.
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.
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.
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.
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.
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.
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.
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).
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.