Una lista de los refactors más comunes que valen la pena en código real de React y TypeScript - cada uno con una etiqueta en negrita, una rationale de una sola oración y un ejemplo breve antes/después. Úsalo como una pre-checklist de PR, una referencia de revisión de código, o una auditoría de búsqueda de anti-patrones en un codebase existente.
Extrae valores mágicos a constantes nombradas - Reemplaza literales dispersos con una única fuente de verdad para que los ajustes no requieran un grep-and-pray en todo el repo.
Reemplaza any con unknown y estrecha - Fuerza a los llamadores a probar la forma antes de usarla, capturando suposiciones malas en el límite en lugar de profundo en la 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(...);}
Prefiere uniones discriminadas sobre props opcionales - Hace que estados imposibles sean no representables para que TypeScript rechace loading: true con data: Foo en tiempo de compilación.
Extrae efectos repetidos en un hook personalizado - Seca la lógica transversal (polling, suscripciones, trampas de foco) y la hace unit-testeable en aislamiento.
Reemplaza prop drilling con context o una store - Elimina props pass-through de componentes que no se preocupan por el valor, para que agregar un nuevo consumidor no toque las capas intermedias.
Calcula estado derivado durante el renderizado en lugar de almacenarlo - Elimina toda una clase de bugs "estos dos estados no coinciden" al hacer que la derivación sea la única fuente de verdad.
Memoiza computaciones costosas con useMemo - Salta trabajo repetido en renderizados causados por estado no relacionado, convirtiendo un lag de entrada visible en retroalimentación instantánea.
Envuelve callbacks en useCallback solo cuando se pasan a hijos memoizados - Mantiene los límites React.memo estables para que los descendientes salten re-renderizados en lugar de romper su memoización.
Divide componentes grandes por responsabilidad - Reduce el alcance del re-renderizado y el alcance de revisión para que un cambio en el header no cause un diff en el footer.
Reemplaza useState enredado con useReducer - Centraliza transiciones relacionadas en una función para que los invariantes se puedan aplicar en lugar de dispersos en los handlers.
Convierte componentes de clase a componentes de función - Desbloquea hooks, bundles más pequeños, y patrones modernos como Suspense y Server Components.
Usa as const para inferencia literal - Fija valores de string y array a sus tipos literales exactos para que fluyan a través de genéricos en lugar de ampliarse a string.
Reemplaza enum con una unión de objeto as const - Produce mejor tree-shaking, JSON más amigable, y tipos más claros sin los quirks de runtime de TypeScript enum.
// beforeenum Status { Open, Closed }// afterconst Status = { Open: "open", Closed: "closed" } as const;type Status = (typeof Status)[keyof typeof Status];
Eleva tipos compartidos a un módulo dedicado - Previene drift de tipos cuando la misma forma se redeclara en tres componentes con campos ligeramente diferentes.
Usa satisfies para validar sin ampliar - Mantiene el tipo inferido estrecho mientras aún verificas que el valor se conforme, para que el autocomplete se mantenga agudo.
Reemplaza keys de índice de array con IDs estables - Arregla bugs de reorden, pérdida de foco de input, y glitches de animación causados por React reutilizando el nodo DOM incorrecto.
Reemplaza fetches con useEffect con TanStack Query (o SWR) - Obtén caching, deduplicación, retries, y cancellación de solicitudes gratis, eliminando docenas de líneas de boilerplate de loading/error.
Valida datos externos con Zod en el límite - Convierte crashes "undefined is not a function" profundos en la UI en un único y explícito parse failure en el borde.
const User = z.object({ id: z.string(), email: z.string().email() });const user = User.parse(await res.json());
Divide un mega-context en contexts dirigidos - Detiene que cada consumidor se re-renderice cuando un slice no relacionado cambia, convirtiendo una app lenta en una snappy.
Extrae lógica de forma en react-hook-form + Zod - Elimina la turbulencia de input controlado, centraliza validación, y hace la forma declarativa en lugar de imperativa.
const form = useForm<FormValues>({ resolver: zodResolver(Schema) });<input {...form.register("email")} />
Reemplaza ternarios anidados con early returns - Aplana branching para que el happy path lea de arriba a abajo sin sostener un parse tree en tu cabeza.
Convierte ops DOM imperativas a state declarativo - Se alinea con el modelo mental de React para que los re-renderizados no puedan accidentalmente deshacer una mutación manual de DOM.
Usa optional chaining y nullish coalescing - Reemplaza guard pyramids con una sola expresión que aún maneja explícitamente los casos null/undefined.
// beforeconst name = user && user.profile && user.profile.name ? user.profile.name : "guest";// afterconst name = user?.profile?.name ?? "guest";
Mueve UI estática a un Server Component - Envía menos JavaScript al cliente y mantiene data-fetching cerca de la fuente sin una roundtrip de hidratación.
// 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-carga rutas y widgets pesados - Mantiene el bundle inicial pequeño para que la primera pintura interactiva no espere en una librería de gráficos que el usuario puede nunca abrir.
Reemplaza React.FC con un tipo de prop explícito - Te da control sobre el contrato children y coincide con el consenso comunitario actual sobre tipado de componentes.
Suelta banderas de loading pareando Suspense con una librería de datos - Elimina ramas isLoading a favor de un único fallback boundary, permitiendo a los componentes asumir que los datos están presentes.
<Suspense fallback={<Skeleton />}> <UserProfile id={id} /> {/* reads data via `use` or a suspending query */}</Suspense>
Tier de tipos (2, 3, 12-15, 28): ganancias baratas en tiempo de compilación - coloca estas primero; desbloquean refactors más seguros aguas abajo.
Tier de render-correctness (6, 10, 16, 23, 24): elimina clases de bugs, no solo ruido - prioriza en cualquier componente que haya registrado regresiones.
Tier de rendimiento (7-9, 19, 26, 27, 30): solo después de una medición real - no especules; perfilacea primero.
Tier de datos (17, 18, 30): usualmente la ganancia más grande de un solo PR en codebases legacy - apunta a una área a la vez.
Tier de estructura (1, 4, 5, 11, 20, 21, 22, 25, 29): se coloca junto con el trabajo de feature; resiste la tentación de agruparlas en un mega-refactor PR.
Refactoring sin tests - Las extracciones puras de render son seguras; los movimientos de lógica no. Arreglo: coloca un test de characterización antes de tocar comportamiento.
Memoización prematura - useMemo/useCallback en todas partes agrega ruido sin ganancias medibles. Arreglo: solo memoiza después de un profiler trace o cuando un downstream React.memo depende de estabilidad referencial.
Mega-PR refactors - Un "cleanup PR" con 40 archivos es unreviewable y conflict-prone. Arreglo: una decisión por PR, stacked si es necesario.
Refactor para features futuras - Formar código para una feature que puede nunca enviarse es sunk cost. Arreglo: refactor el minuto antes de que la feature aterrice, no semanas antes.
Type-only refactors persiguiendo 100% strictness - Reemplazar cada any de una sola vez crea diffs enormes sin cambio de comportamiento. Arreglo: habilita banderas strict incrementalmente y corrige drift en el límite del módulo.
Comienza con el tier de tipos (elementos 2, 3, 12-15, 28) - son solo tiempo de compilación, bajo riesgo, y hacen el resto más seguro.
Luego toca los refactors de capa de datos (elementos 17, 18, 30) para la mayor reducción de código de un único PR.
Deja rendimiento (elementos 7-9) para último - solo toca después de un real profiler trace.
¿Cuándo es `useMemo` o `useCallback` realmente worth it?
Cuando el valor se pasa a un hijo React.memo'd que de otra manera se re-renderizaría en cada parent render.
Cuando la computación en sí es measurably expensive (sorting/filtering listas grandes, derivación de datos pesada).
No como default - la memoización especulativa agrega ruido y puede incluso ralentizar las cosas vía deps array churn.
¿Por qué preferir uniones discriminadas sobre props opcionales?
Hacen que estados imposibles sean no representables - loading: true con data: Foo simplemente no type-checkará.
Los consumidores pueden exhaustivamente switchear en el discriminador y dejar que TypeScript atrape casos faltantes.
Los props opcionales te dejan accidentalmente olvidar limpiar error cuando loading reinicia - las uniones no.
¿Siempre debo reemplazar `enum` con objetos `as const`?
Para lookups tipo string, sí - mejor tree-shaking, JSON más limpio, sin sorpresas de reverse-mapping enum de runtime.
Los numeric enums pueden quedarse si estás haciendo bitwise flags o necesitas iteración sobre valores.
La mayor ganancia es que los objetos as const componen naturalmente con satisfies y tipos de template literal.
¿Cómo sé cuándo dividir un big context?
Si cambiar un slice (theme) re-renderiza consumidores de slices no relacionados (cart), has superado un single context.
Un quick React DevTools profile en una interacción dirigida mostrará la cascada de re-renderizado claramente.
Divide por frecuencia de actualización: valores que se mueven lentamente (auth, theme) en un context, valores que se mueven rápido (cart, selection) en otro o una store.
¿No es `any` a veces inevitable?
Casi nunca en código de producto - unknown más una función de narrowing es más seguro en cada caso en que any es tentador.
Los internals de librería que lidia con generics verdaderamente dinámicos (Zod, TanStack Query) son la excepción honesta.
Si debes usar any, confínalo a una sola función y comenta por qué - no lo dejes filtrar a través de límites de módulo.
¿Cuándo debo refactor un componente de clase a un componente de función?
Cuando ya estás tocando el archivo - no lo hagas como un standalone PR a menos que un hook sea específicamente necesario.
Cuando los lifecycle methods son un pobre fit para la lógica (p.ej., subscription cleanup disperso en tres métodos).
Nunca como un Friday-afternoon sweep - las clases funcionan bien; el valor está en desbloquear hooks o features modernas.
¿Qué tan agresivo debo ser sobre eliminar `useEffect` para data fetching?
Muy - el fetching basado en effect se pierde caching, dedupe, retries, y cancellación que las librerías te dan gratis.
Migra un hot path (usualmente el dashboard o list view) y mira cómo desaparece el boilerplate de loading-state.
Reserva useEffect para true side-effects al mundo exterior: suscripciones, timers, APIs imperativas.
¿Por qué no solo enviar un big refactor PR?
Los PRs grandes son unreviewable - los revisores los rubber-stamp y regresos sutiles se cuelan.
Los conflictos de rebase componen con team velocity; los PRs pequeños aterrizan antes de que los conflictos importen.
Una-decisión-por-PR te da un clean git history y un fácil revert path cuando algo se rompe.
¿Cuál es la cobertura de test mínima antes de refactoring?
Al menos un integration test por user-visible path que estés tocando - no importa si es Playwright o Testing Library.
Los refactors type-only son seguros sin tests porque un miss surfaceará en tiempo de compilación.
Los logic moves (reducer consolidation, hook extraction) necesitan un before-test que bloquee el comportamiento actual en lugar.
¿Cómo dejo de acumular llamadas `useState` en un single componente?
Si dos o más campos actualizan juntos, fold ellos en un useReducer con acciones nombradas.
Si el valor derivado puede computarse desde estado existente, cómputalo durante render en lugar de almacenarlo.
Si el state no conduce rendering, considera useRef en su lugar.
¿Cuál es la manera más barata de encontrar candidatos de refactor en un codebase existente?
Grep para any, as any, @ts-expect-error, y // eslint-disable - marcan la rot.
Ejecuta el React DevTools profiler en los tres top user paths y busca re-renderizados evitables.
Ordena archivos por line count; cualquier cosa sobre ~400 líneas es casi siempre un candidato split.