Lista de verificación para revisiones de código en React: estado derivado, reducidores, efectos y memoización.
Aprende a identificar siete hallazgos comunes en las revisiones de React, desde estado duplicado y obtención de datos manual hasta el uso incorrecto de efectos y memorización excesiva, y qué escribir en su lugar.
Los desarrolladores con algunos años de experiencia en React suelen saber cómo fluyen las props, cómo funcionan los hooks y por qué el estado nunca debe modificarse directamente. Sin embargo, las revisiones de código siguen revelando los mismos problemas en sus pull requests: componentes que funcionan pero son frágiles, difíciles de probar, sobreconstruidos o complicados de extender. Superar esa etapa no se trata tanto de aprender APIs poco comunes como de afinar el modelo acerca de dónde reside el estado, para qué sirven los efectos y cuándo vale la pena optimizar. Esta lista de verificación analiza siete problemas recurrentes, utilizando una lista de tareas en TypeScript como ejemplo práctico, y muestra el patrón a seguir en cada caso.
1. Estado que debería derivarse
La regla fundamental del estado en React es mantener una única fuente de verdad. Cada vez que la misma información se encuentra en dos lugares, las copias eventualmente estarán desacordadas.
Así es como se manifiesta
Una lista de tareas pendientes que necesita tanto los elementos no completados como los completados suele terminar con dos arrays en el estado:
// ❌ BAD: Storing derived state in two separate places
const [todos, setTodos] = useState<Todo[]>([]);
const [completedTodos, setCompletedTodos] = useState<Todo[]>([]);
Ahora cada cambio tiene que aplicarse dos veces. Si un usuario renombra una tarea y el código actualiza todos pero olvida completedTodos, la interfaz muestra el título antiguo en una lista y el nuevo en la otra.
Qué escribir en su lugar
Mantener un solo array en el estado y calcular el subconjunto completado mientras se renderiza:
// ✅ GOOD: Single source of truth with derived state
const [todos, setTodos] = useState<Todo[]>([]);
// Derived value recalculated on every render (inexpensive computation)
const completedTodos = todos.filter((todo) => todo.completed);
Filtrar un array en cada renderización es poco costoso para tamaños típicos, y la lista derivada nunca se desincronizará porque se vuelve a calcular a partir de la fuente cada vez. Si el cálculo se vuelve realmente costoso, por ejemplo al filtrar decenas de miles de elementos, envuélvalo en useMemo; de lo contrario, una expresión simple es más clara.
2. Obtención manual de datos en useEffect
Las llamadas manuales a fetch dentro de useEffect parecen sencillas, pero tienden a generar condiciones de carrera, carecen de caché y requieren que el estado de carga y de errores se gestione manualmente en cada componente.
Así es como se ve
Este efecto carga la lista una sola vez y utiliza una bandera isMounted para evitar establecer estado después de que el componente se desmonte:
// ❌ BAD: Boilerplate-heavy, error-prone, no caching or deduplication
useEffect(() => {
let isMounted = true;
fetch('/api/todos')
.then((res) => res.json())
.then((data) => {
if (isMounted) setTodos(data);
});
return () => { isMounted = false; };
}, []);
Aún así tiene deficiencias. No hay manejo de errores, no existe estado de carga, no hay deduplicación cuando dos componentes solicitan los mismos datos, no hay caché entre pantallas ni posibilidad de reintentar. La bandera isMounted solo suprime la actualización tardía; no cancela la solicitud, algo que sí haría un AbortController.
Qué escribir en su lugar
Utilice una biblioteca diseñada para el estado del servidor, como TanStack Query, SWR o RTK Query. La llamada equivalente de TanStack Query declara una clave de caché y una función de obtención de datos, y recibe de vuelta los datos, el estado de carga y el estado de error:
// ✅ GOOD: Declarative, automatically cached, auto-retried
const { data: todos, isLoading, error } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
Bibliotecas como estas gestionan automáticamente la revalidación en segundo plano, la deduplicación de solicitudes, las reintentos y el uso compartido del caché, además de ofrecer soporte estructurado para actualizaciones optimistas e integración con límites de errores. El código del componente se reduce a una descripción de lo que necesita.
3. Un montón de llamadas a useState donde cabría un reducer
useState es ideal para valores simples como un interruptor o un campo de texto. Una vez que las actualizaciones dependen de valores anteriores, o el estado incluye arrays anidados y objetos modificados por varias acciones diferentes, un conjunto de funciones de configuración hace que los componentes se vuelvan voluminosos y propensos a errores por estado obsoleto.
Así es como se ve
Un único componente declara de cinco a diez ganchos useState y los actualiza desde numerosas funciones de callback para agregar, editar, alternar y eliminar elementos. Las reglas de negocio terminan dispersas entre los manejadores de eventos.
Qué escribir en su lugar
useReducer traslada las transiciones de estado fuera del componente a una sola función. El reductor mostrado a continuación utiliza una unión discriminada para sus acciones, de modo que TypeScript conoce el tipo del contenido para cada caso. Al agregar un elemento a la lista de tareas, se agrega un nuevo objeto con un ID generado; al alternar, se recorre la lista y se modifica el elemento correspondiente. Tenga en cuenta que la línea de alternación contiene un error tipográfico: ...todo.completed debería ser !todo.completed, que representa la negación lógica; de lo contrario, el código no se compila.
type Action =
| { type: 'ADD_TODO'; payload: string }
| { type: 'TOGGLE_TODO'; payload: string };
function todoReducer(state: Todo[], action: Action): Todo[] {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: crypto.randomUUID(), text: action.payload, completed: false }];
case 'TOGGLE_TODO':
return state.map((todo) =>
todo.id === action.payload ? { ...todo, completed: ...todo.completed } : todo
);
default:
return state;
}
}
Los reducers son útiles de dos maneras:
- Probabilidad de pruebas. Un reducer es una función pura, por lo que se puede probar sin renderizar nada ni simular React.
- Prediccibilidad. Cada actualización pasa por una acción con nombre en lugar de por setters arbitrarios llamados desde callbacks dispersos, lo que facilita el seguimiento y registro de los cambios.
4. Sin pruebas porque “hacer pruebas es un proyecto enorme”
Muchos equipos consideran las pruebas como algo todo o nada: o bien una configuración completa de tipo end-to-end, que resulta abrumadora, o nada en absoluto. Un enfoque mejor comienza con las pruebas que son más fáciles de escribir y de mayor valor para mantener.
Da prioridad en este orden:
- Lógica de negocio independiente. Ayudantes, formateadores, validadores y utilidades que se encuentran fuera del árbol de componentes.
- Reducers y hooks personalizados. Lógica de transición de estado pura.
@testing-library/react). Una prueba de reducer es un buen primer paso. Se crea un estado inicial, se envía una acción directamente a través de la función y se verifica el resultado, sin que haya renderizado alguno:
// Example: Testing a reducer takes 2 minutes and provides huge peace of mind
test('should toggle todo status', () => {
const initialState = [{ id: '1', text: 'Write Article', completed: false }];
const newState = todoReducer(initialState, { type: 'TOGGLE_TODO', payload: '1' });
expect(newState[0].completed).toBe(true);
});
Una prueba como esta lleva minutos de escribir y protege las reglas en las que depende toda la funcionalidad. Una vez corregido el error de escritura en el reducer del apartado anterior, esta prueba es exactamente la que lo habría detectado.
5. Uso de useEffect para sincronizar React consigo mismo
useEffect es el hook más mal utilizado en React. Un patrón frecuente es usarlo para copiar o ajustar el estado interno cuando cambia una propiedad, lo que provoca un renderizado adicional y un breve momento de UI inconsistente.
Surgen repetidamente dos conceptos erróneos:
- "El estado debe actualizarse en un efecto cuando cambia una propiedad." Por lo general, no. Calcule el valor durante la renderización, o bien, si el estado de un hijo debe reiniciarse, dele una
keyque cambie junto con la propiedad relevante para que React lo vuelva a montar. - "El comportamiento al hacer clic debe estar en un efecto." La lógica desencadenada por una acción del usuario debe encontrarse en el manejador de eventos, como
onClick, y no en un efecto que supervise el cambio de estado provocado por el clic.
El principio rector: los efectos son una vía de escape para sincronizarse con sistemas externos a React, como las APIs del navegador, suscripciones, herramientas de análisis o widgets DOM de terceros. Los datos que ya posee React pueden transformarse sin necesidad de ellos. Para un análisis más detallado con ejemplos antes y después, consulte dejar de sincronizar el estado con useEffect.
6. Memorización automática de todo por defecto
El trabajo relacionado con el rendimiento tiende a oscilar entre dos extremos: ignorar por completo el costo de renderizado, o envolver cada componente en React.memo y cada función en useCallback.
Por qué la memorización generalizada tiene efectos negativos
useMemo y useCallback no son gratuitos. React debe almacenar las dependencias y compararlas en cada renderizado. En el caso de un componente cuyo renderizado es rápido, esa gestión puede costar tanto como el propio renderizado que se intenta omitir, además de dificultar la lectura del código. La memorización también solo es útil cuando todas las propiedades son estables; un objeto inline o una función de flecha pasada a un hijo memorizado anula sus efectos.
Un enfoque más deliberado
- Analice primero. El perfilador de React DevTools muestra qué componentes se renderizan con demasiada frecuencia o demasiado lentamente.
- Corrija la estructura antes de añadir memorización. Trasladar el estado al componente que lo utiliza, o pasar subárboles costosos como
children, a menudo evita renderizos innecesarios sin necesidad de memorización alguna.
React.memo a listas largas y gráficos complejos, y mantiene estables sus props con useCallback y useMemo en esos casos.7. Tratar el CSS y la accesibilidad como si fuera trabajo de otro
Los ingenieros expertos en React también son competentes en la plataforma del navegador. Los componentes solo son tan buenos como el marcado y los estilos que generan.
Áreas que vale la pena dominar:
- Diseño de layouts modernos. Conoce bien Flexbox y CSS Grid para que la posición absoluta ya no sea la solución de último recurso en los diseños habituales.
- Estilización enfocada. Mantén los estilos junto a los componentes mediante CSS Modules, Tailwind CSS o CSS-in-JS, en lugar de depender de hojas de estilo globales grandes donde los nombres de clases chocan.
- Accesibilidad y usabilidad. Utilice elementos semánticos, como un verdadero
<button>en lugar de<div onClick={...}>. Asegúrese de que la navegación por teclado con Tab, Enter y Espacio funcione en todas partes. Proporcione estilos visibles de:focus-visibley estados interactivos claros, como un cursor de puntero en los elementos clickeables.
Lista de verificación para la revisión
Al revisar una solicitud de pull en React, busque estas sustituciones:
- Valores derivados almacenados en hooks adicionales de
useState: calcúlelos durante el renderizado. - Llamadas manuales a la API dentro de
useEffect: utilice TanStack Query, SWR o RTK Query. - Muchas llamadas a
useStatepara arrays y objetos complejos: traslade las transiciones auseReducery pruebe el reductor. - No se realizan pruebas porque la configuración parece costosa: comience con funciones puras, reducers y hooks personalizados.
useEffectse utiliza para sincronizar el estado: obtenga el valor o guárdelo en el manejador de eventos.useCallbackyReact.memoen todas partes: realice un análisis previo antes de memorizar.- Elementos
divclickeables y falta de estilos para el foco: utilice HTML semántico y verifique el acceso por teclado.
Conclusión
La mayoría de estos problemas comparten una causa raíz: el estado o la lógica que se encuentra en más lugares de los necesarios, o que se ejecuta con mayor frecuencia de lo requerido. Mantener una única fuente de verdad, permitir que las herramientas diseñadas específicamente gestionen los datos del servidor, aislar las transiciones en funciones puras y reservar los efectos para el mundo exterior reducen la cantidad de componentes dinámicos. El código con menos componentes dinámicos es más fácil de probar, más fácil de optimizar cuando es necesario y genera mayor confianza en el siguiente revisor.