A list of the most common refactors that pay off on real React and TypeScript code - each with a bold label, a one-sentence rationale, and a short before/after snippet. Use it as a pre-PR checklist, a code review reference, or a spike-hunting audit on an existing codebase.
Extract magic values to named constants - Replaces scattered literals with a single source of truth so tweaks don't require a grep-and-pray across the repo.
Replace any with unknown and narrow - Forces callers to prove the shape before use, catching bad assumptions at the boundary instead of deep in the UI.
// beforefunction parse(data: any) { return data.items.map(...); }// afterfunction parse(data: unknown) { if (!isPayload(data)) throw new Error("bad payload"); return data.items.map(...);}
Prefer discriminated unions over optional props - Makes impossible states unrepresentable so TypeScript rejects loading: true with data: Foo at compile time.
Replace prop drilling with context or a store - Removes pass-through props from components that don't care about the value, so adding a new consumer doesn't touch the middle layers.
Compute derived state during render instead of storing it - Eliminates a whole class of "these two states disagree" bugs by making the derivation the single source of truth.
Memoize expensive computations with useMemo - Skips repeat work on renders caused by unrelated state, turning a visible input lag into instant feedback.
Wrap callbacks in useCallback only when passed to memoized children - Keeps React.memo boundaries stable so descendants skip rerenders instead of breaking their memoization.
Split oversized components by responsibility - Narrows the rerender scope and the review scope so a change to the header doesn't cause a diff in the footer.
Replace tangled useState with useReducer - Centralizes related transitions in one function so invariants can be enforced instead of scattered across handlers.
Use as const for literal inference - Pins string and array values to their exact literal types so they flow through generics instead of widening to string.
Replace enum with an as const object union - Produces better tree-shaking, friendlier JSON, and clearer types without TypeScript's enum runtime quirks.
// beforeenum Status { Open, Closed }// afterconst Status = { Open: "open", Closed: "closed" } as const;type Status = (typeof Status)[keyof typeof Status];
Lift shared types to a dedicated module - Prevents type drift when the same shape is redeclared in three components with slightly different fields.
Replace useEffect fetches with TanStack Query (or SWR) - Gets caching, deduplication, retries, and request cancellation for free, deleting dozens of lines of loading/error boilerplate.
Validate external data with Zod at the boundary - Converts "undefined is not a function" crashes deep in the UI into a single, explicit parse failure at the edge.
const User = z.object({ id: z.string(), email: z.string().email() });const user = User.parse(await res.json());
Split a mega-context into targeted contexts - Stops every consumer from rerendering when an unrelated slice changes, turning a sluggish app into a snappy one.
Extract form logic into react-hook-form + Zod - Removes controlled-input churn, centralizes validation, and makes the form declarative instead of imperative.
const form = useForm<FormValues>({ resolver: zodResolver(Schema) });<input {...form.register("email")} />
Replace nested ternaries with early returns - Flattens branching so the happy path reads top-to-bottom without holding a parse tree in your head.
Use optional chaining and nullish coalescing - Replaces guard pyramids with a single expression that still handles the null/undefined cases explicitly.
// beforeconst name = user && user.profile && user.profile.name ? user.profile.name : "guest";// afterconst name = user?.profile?.name ?? "guest";
Move static UI to a Server Component - Ships less JavaScript to the client and keeps data-fetching close to the source without a hydration roundtrip.
// app/page.tsx (Server Component by default in App Router)export default async function Page() { const posts = await db.posts.findMany(); return <PostList posts={posts} />;}
Lazy-load heavy routes and widgets - Keeps the initial bundle small so the first interactive paint doesn't wait on a chart library the user may never open.
Replace React.FC with an explicit prop type - Gives you control over the children contract and matches the current community consensus on component typing.
Drop loading flags by pairing Suspense with a data library - Removes isLoading branches in favor of a single fallback boundary, letting components assume data is present.
<Suspense fallback={<Skeleton />}> <UserProfile id={id} /> {/* reads data via `use` or a suspending query */}</Suspense>
Refactoring without tests - Pure-render extractions are safe; logic moves aren't. Fix: land a characterization test before touching behavior.
Premature memoization - useMemo/useCallback everywhere adds noise without measurable wins. Fix: only memoize after a profiler trace or when a downstream React.memo depends on referential stability.
Mega-PR refactors - A "cleanup PR" with 40 files is unreviewable and conflict-prone. Fix: one decision per PR, stacked if needed.
Refactor for future features - Shaping code for a feature that may never ship is sunk cost. Fix: refactor the minute before the feature lands, not weeks before.
Type-only refactors chasing 100% strictness - Replacing every any in one go creates huge diffs with no behavior change. Fix: enable strict flags incrementally and fix drift at the module boundary.