Search across all documentation pages
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.
// before
if (retries < 3) retry();
// after
const MAX_RETRIES = 3;
if (retries < MAX_RETRIES) retry();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.
// before
function parse(data: any) { return data.items.map(...); }
// after
function parse(data: unknown) {
if (!isPayload(data)) throw
Prefer discriminated unions over optional props - Makes impossible states unrepresentable so TypeScript rejects loading: true with data: Foo at compile time.
// before
type State = { loading: boolean; data?: Foo; error?: Error };
// after
type State =
| { status: "loading" }
Extract repeated effects into a custom hook - DRYs cross-cutting logic (polling, subscriptions, focus traps) and makes it unit-testable in isolation.
// before
useEffect(() => {
const id = setInterval(fetchStatus, 5000);
return () => clearInterval(id);
}, []);
// after
usePolling(fetchStatus, 5000);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.
// before
<Page user={user}><Toolbar user={user}><Avatar user={user} /></Toolbar></Page>
// after
<UserProvider value={user}><Page><Toolbar><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.
// before
const [items, setItems] = useState<Item[]>([]);
const [count, setCount] = useState(0); // drifts
// after
const [items
Memoize expensive computations with useMemo - Skips repeat work on renders caused by unrelated state, turning a visible input lag into instant feedback.
const sorted = useMemo(() => bigList.sort(compareFn), [bigList]);Wrap callbacks in useCallback only when passed to memoized children - Keeps React.memo boundaries stable so descendants skip rerenders instead of breaking their memoization.
const onSelect = useCallback((id: string) => setSelected(id), []);
return <MemoizedList onSelect={onSelect} />;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.
// before: 400-line <Dashboard /> doing header, filters, table, pagination
// after
<Dashboard><Header /><Filters /><Table /><Pagination /></Dashboard>Replace tangled useState with useReducer - Centralizes related transitions in one function so invariants can be enforced instead of scattered across handlers.
const [state, dispatch] = useReducer(cartReducer, initialCart);
dispatch({ type: "add", item });Convert class components to function components - Unlocks hooks, smaller bundles, and modern patterns like Suspense and Server Components.
// before
class Timer extends React.Component { /* lifecycle methods */ }
// after
function Timer() { useEffect(() => { /* ... */ }, []); return <span /> }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.
// before
const roles = ["admin", "user"]; // string[]
// after
const roles = ["admin", "user"] as const; // readonly ["admin", "user"]Replace enum with an as const object union - Produces better tree-shaking, friendlier JSON, and clearer types without TypeScript's enum runtime quirks.
// before
enum Status { Open, Closed }
// after
const Status = { Open: "open", Closed: "closed" } as const;
type Status = (typeof Status)[keyofLift shared types to a dedicated module - Prevents type drift when the same shape is redeclared in three components with slightly different fields.
// src/types/user.ts
export interface User { id: string; email: string; roles: Role[] }Use satisfies to validate without widening - Keeps the narrow inferred type while still checking the value conforms, so autocomplete stays sharp.
const routes = {
home: "/",
admin: "/admin",
} satisfies Record<string, `/${string}`>;Replace array-index keys with stable IDs - Fixes reorder bugs, input focus loss, and animation glitches caused by React reusing the wrong DOM node.
// before
items.map((item, i) => <Row key={i} {...item} />)
// after
items.map((item) => <Row key={item.id} {...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.
const { data, isLoading } = useQuery({ queryKey: ["user", id], queryFn: () => fetchUser(id) });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.
<AuthProvider>
<ThemeProvider>
<CartProvider>{children}</CartProvider>
</ThemeProvider>
</AuthProvider>Colocate state with the component that owns it - Shrinks reasoning scope and makes deletion trivial when the feature goes away.
// before: modal open state in a global store
// after: useState inside <Modal /> itself
const [open, setOpen] = useState(false);Replace boolean-prop soup with a variant union - Eliminates impossible combinations like primary && danger && outline at the type level.
// before
<Button primary danger outline />
// after
<Button variant="danger-outline" />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.
// before
return loading ? <Spinner /> : error ? <Error /> : data ? <View data={data} /> : null;
// after
if (loading) return <
Convert imperative DOM ops to declarative state - Aligns with React's mental model so rerenders can't accidentally undo a manual DOM mutation.
// before
useEffect(() => { ref.current!.classList.add("open"); }, [open]);
// after
<div className={open ? "open" : ""} />Use optional chaining and nullish coalescing - Replaces guard pyramids with a single expression that still handles the null/undefined cases explicitly.
// before
const name = user && user.profile && user.profile.name ? user.profile.name : "guest";
// after
const 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.
const Chart = lazy(() => import("./Chart"));
<Suspense fallback={<Spinner />}><Chart /></Suspense>Replace React.FC with an explicit prop type - Gives you control over the children contract and matches the current community consensus on component typing.
// before
const Card: React.FC<{ title: string }> = ({ title, children }) => <div>{title}{children}</div>;
// after
function Card({
Wrap risky subtrees in an error boundary - Isolates blast radius so a broken chart doesn't take down the whole dashboard.
<ErrorBoundary fallback={<ChartError />}>
<Chart />
</ErrorBoundary>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>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.any in one go creates huge diffs with no behavior change. Fix: enable strict flags incrementally and fix drift at the module boundary.| Alternative | Use When | Don't Use When |
|---|---|---|
| Codemods (jscodeshift, ts-morph) | The refactor is mechanical and repeats across hundreds of files. | The change needs human judgment per site. |
| Big-bang rewrite | The old code is fundamentally unsafe and no incremental path exists. | Incremental refactors are possible - they almost always are. |
| Strangler fig pattern | You're migrating off a legacy framework or pattern gradually. | The codebase is small enough to refactor in one sweep. |
| Deprecation comments + lint rules | You can't refactor all call sites immediately but want to prevent new uses. | The pattern is already fully removable. |
useMemo or useCallback actually worth it?React.memo'd child that would otherwise rerender on every parent render.loading: true with data: Foo simply won't type-check.error when loading restarts - unions won't.enum with as const objects?as const objects compose naturally with satisfies and template literal types.any sometimes unavoidable?unknown plus a narrowing function is safer in every case any is tempting.any, confine it to a single function and comment why - don't let it leak across module boundaries.useEffect for data fetching?useEffect for true side-effects to the outside world: subscriptions, timers, imperative APIs.useState calls in a single component?useReducer with named actions.useRef instead.any, as any, @ts-expect-error, and // eslint-disable - they mark the rot.