Veinte solicitudes de refactorización del mundo real tal como llegan - algunas como comentarios de revisión de PR, otras como quejas de stakeholders que resultan ser problemas de forma del código, otras como tareas de limpieza de un tech lead. Cada entrada muestra la solicitud tal como está escrita, el diagnóstico de lo que realmente está mal, una refactorización concisa y por qué la nueva forma es mejor.
Trátala como una checklist: repasa primero los 20 títulos y marca cuáles describen código que mantienes ahora mismo.
La línea "Por qué funciona" es la parte que debes memorizar - te indica cuándo aplicar la misma refactorización en otro archivo.
La mayoría de las refactorizaciones aquí son del tamaño de un PR. Si un solo ítem se convierte en un proyecto de varios días, el diagnóstico probablemente es incorrecto.
Envía un refactor por PR. Agrupar una conversión a Server Component con un pase de tipado hace la revisión hostil.
"El archivo <Dashboard /> sigue creciendo cada sprint. Los revisores pasan 40 minutos en cada PR y aun así se les escapan cosas. ¿Podemos limpiarlo?"
Diagnóstico: Un componente renderiza el layout, obtiene tres recursos, los formatea, registra analíticas y posee cuatro piezas de state de UI. Viola la responsabilidad única en todos los ejes.
Por qué funciona: Cada hijo tiene una sola razón para cambiar. Los revisores hacen diff de un panel a la vez, y el Server Component puede obtener datos en paralelo sin un useEffect.
"La prop currentUser se enhebra por Layout, Sidebar, Nav, NavItem y finalmente LogoutButton. Añadir un campo al objeto user implica editar cinco archivos."
Diagnóstico: Prop drilling. Los componentes intermedios no usan user - solo la reenvían.
"Los usuarios se quejan de que la página de producto muestra un skeleton de carga medio segundo incluso cuando ya la han visitado. ¿Podemos hacer que se sienta instantánea?"
Diagnóstico: Un Client Component obtiene datos en useEffect. El servidor no devuelve datos, el cliente se monta y luego obtiene. El flash es inevitable con esta forma.
Por qué funciona: Mover la obtención a un Server Component integra los datos en la primera respuesta HTML. Sin spinner, sin cascada en el cliente, sin useEffect.
Por qué funciona:as const convierte los valores en tipos literales. Renombrar la constante falla al compilar en cada consumidor en lugar de fallar en silencio.
"Los errores en Sentry mencionan Cannot read properties of undefined. El resultado del fetch está tipado como any y accedimos a data.user.profile.name."
Diagnóstico:any cortocircuita cada comprobación. La forma de la respuesta de la API es desconocida para el compilador.
Refactorización:
import { z } from "zod";const User = z.object({ profile: z.object({ name: z.string() }) });const res = await fetch("/api/user");const user = User.parse(await res.json());
Por qué funciona: Parsear en el límite convierte "any" en un tipo conocido. Las respuestas malas lanzan en el borde en lugar de crashear cuatro componentes más abajo.
Por qué funciona: Las claves con nombre se documentan solas en el sitio de llamada. Añadir una opción nueva no rompe compatibilidad; los valores por defecto viven en un solo lugar.
"Search, el sidebar, la insignia del inbox y el header llaman todos a /api/notifications. Se desincronizan constantemente y uno siempre tiene caché obsoleta."
Diagnóstico: Lógica de carga de datos duplicada. Cada componente tiene su propio useEffect, su propio state de carga y su propio comportamiento de reintento.
"El formulario de registro sigue creciendo a medida que añadimos campos. Las reglas de validación viven en una escalera gigante de if y enviamos bugs de validación en cada release."
Diagnóstico: Gestión de state imperativa para un formulario que debería ser declarativo.
Refactorización:
"use client";import { useForm } from "react-hook-form";import { zodResolver } from "@hookform/resolvers/zod";import { z } from "zod";const Schema = z.object({ email: z.string().email(), password: z.string().min(12),});const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(Schema),});
Por qué funciona: El esquema es la especificación. Validación, visualización de errores y tipos salen de la misma fuente - añadir un campo es un cambio de una línea.
"ESLint avisa sobre UNSAFE_componentWillReceiveProps en <NotificationBell />. El tech lead dice que lo convirtamos antes de la actualización a React 19."
Diagnóstico: Ciclo de vida de clase legacy que no tiene equivalente directo en hooks y falla bajo la doble invocación de Strict Mode.
"Esta tarjeta tiene loading ? skeleton : error ? errorView : empty ? emptyState : data ? <Card /> : null y nadie puede leerlo."
Diagnóstico: Renderizado condicional comprimido en una sola expresión.
Refactorización:
function CardView({ state }: { state: ViewState }) { if (state.kind === "loading") return <Skeleton />; if (state.kind === "error") return <ErrorView error={state.error} />; if (state.kind === "empty") return <EmptyState />; return <Card data={state.data} />;}
Por qué funciona: Una unión discriminada + returns tempranos se lee de arriba a abajo. Cada rama es editable de forma independiente y la unión te obliga a manejar cada caso.
Por qué funciona: Las refs dejan que la librería se suscriba al mismo ciclo de vida que React ya gestiona. Sin querySelector, sin nodos desmontados, sin roturas en tests.
Por qué funciona: La ref comparte una identidad entre renders, así que el intervalo lee el valor más reciente sin volver a suscribirse en cada cambio de state.
Por qué funciona: Los valores derivados calculados durante el renderizado siempre están frescos. No hay nada que sincronizar porque solo hay una fuente.
Por qué funciona:useCallback preserva la identidad de la función entre renders. La comparación superficial de memo ahora ve la misma prop y omite el subárbol.
Por qué funciona: Las clases de Tailwind se resuelven en tiempo de build y se deduplican en el bundle. Los tokens de espaciado se vuelven aplicables en lugar de artesanales.