Inicio / Artículos

Elegir la herramienta adecuada de React: Derive, Handle, Fetch, Defer o Effect

Una guía de decisiones para reemplazar las llamadas reflexivas a useEffect por valores derivados, manejadores de eventos, una capa de datos, useTransition, useMemo medido y las API de React 19.

Editor: REACTAPP.TOP8 min de lectura1896 palabras

Fuentes: Stop Using `useEffect` for Everything: 7 React Patterns That Make Your Code Simpler

Muchos componentes de React recopilan llamadas a useEffect para realizar búsquedas, reaccionar ante propiedades, calcular valores y mantener alineado el estado. Unos meses después abres el archivo y encuentras cuatro efectos, tres variables de estado derivadas, dos indicadores de carga y un error que nadie puede explicar. Esta guía te ofrece una sola pregunta que hacer antes de escribir cualquier hook: “¿Realmente se necesita un efecto?”, además de siete patrones concretos para saber qué utilizar cuando la respuesta es no.

Los valores derivados pertenecen a render, no al estado

El uso incorrecto más frecuente es almacenar algo que puede calcularse. Supongamos que muestras productos filtrados por una consulta de búsqueda. Un intento común al principio es agregar una variable de estado para el resultado:

const [filteredProducts, setFilteredProducts] = useState([]);

y luego un efecto que lo vuelve a calcular cada vez que cambian las entradas:

useEffect(() => {
  setFilteredProducts(
    products.filter(product =>
      product.name.toLowerCase().includes(query.toLowerCase())
    )
  );
}, [products, query]);

Esto funciona, pero filteredProducts no es un estado independiente. Es una función pura de los valores que ya se tienen:

products
+
query
=
filteredProducts

Al almacenarlo, React primero muestra resultados obsoletos, luego ejecuta el Effect y vuelve a renderizar. En su lugar, calcúlalo directamente dentro del cuerpo del componente:

const filteredProducts = products.filter(product =>
  product.name.toLowerCase().includes(query.toLowerCase())
);

Al final no hay ninguna variable de estado adicional, ningún Effect, nada que sincronizar y tampoco un segundo renderizado solo para obtener el resultado. Si el filtrado es realmente costoso, por ejemplo con una lista muy grande, envuelve la misma expresión en useMemo:

const filteredProducts = useMemo(() => {
  return products.filter(product =>
    product.name.toLowerCase().includes(query.toLowerCase())
  );
}, [products, query]);

El uso de memo es un detalle. Lo importante es el principio: un valor derivado de props o estado no necesita tener un lugar en el estado.

Las acciones del usuario deben ir en los manejadores de eventos

Otro patrón dirige un clic a través del estado para que un Effect pueda reaccionar ante él. Primero, una bandera:

const [shouldSave, setShouldSave] = useState(false);

luego un Effect que vigila la bandera, además de un manejador que la establece:

useEffect(() => {
  if (shouldSave) {
    saveUser();
    setShouldSave(false);
  }
}, [shouldSave]);function handleSave() {
  setShouldSave(true);
}

No hay razón para el desvío. El clic es el momento en que debe ocurrir la acción, y ya tienes una función que se ejecuta en ese instante:

function handleSave() {
  saveUser();
}

Compara los dos flujos. La versión con bandera pasa por varios pasos antes de que se guarde cualquier cosa, y luego dispara otro renderizado para reiniciarse:

Click
  ↓
State update
  ↓
Render
  ↓
Effect
  ↓
saveUser()
  ↓
Another state update
  ↓
Render

La versión directa tiene un solo paso:

Click
  ↓
saveUser()

La regla general es que los eventos se manejan en manejadores de eventos, y los Effects existen para sincronizarse con sistemas externos a React, no para disfrazar la lógica de eventos ordinaria.

Copia las propiedades al estado solo cuando necesites dos valores

Esta parece inofensiva. Un componente mantiene su propia copia de un nombre:

const [name, setName] = useState('');

y un Effect sobrescribe esa copia cada vez que cambia la propiedad:

useEffect(() => {
  setName(user.name);
}, [user]);

Ahora hay dos fuentes de verdad, y en cualquier momento hay que preguntarse cuál es la correcta:

user.name
     +
name

Si lo único que se hace es mostrar el valor, hay que leerlo directamente desde la propiedad:

return <h2>{user.name}</h2>;

Existe una excepción legítima: un formulario de edición que mantiene deliberadamente una copia temporal y editable de los datos del servidor hasta que el usuario guarda o cancela:

Server data
    ↓
Edit form
    ↓
User changes fields
    ↓
Save / Cancel

Aquí el estado local es correcto porque el borrador realmente es un valor separado con su propio ciclo de vida. La lección no es “nunca copies propiedades al estado”, sino “sabes por qué estás creando una segunda fuente de verdad”. En el caso de los formularios, reiniciar el borrador asignando al formulario una key vinculada al ID del registro suele ser más limpio que sincronizarlo con un Effect; nuestro artículo sobre cómo reemplazar los Effects de sincronización de estado explica en detalle esa técnica.

Deja que la capa de datos se encargue de la obtención de datos

La carga de datos tiende a distribuirse entre varios Effects. Comienza con una solicitud al montar:

useEffect(() => {
  fetchUser();
}, []);

y se convierte en una serie de Effects que cada uno obtiene datos para el mismo usuario:

useEffect(() => {
  fetchOrders();
}, [userId]);useEffect(() => {
  fetchNotifications();
}, [userId]);useEffect(() => {
  fetchPermissions();
}, [userId]);

En ese punto, el componente se ha convertido en una capa de orquestación de API. En aplicaciones más grandes, aquí es donde resulta útil una solución dedicada a la obtención de datos. Dependiendo de su stack tecnológico, esto podría ser:

  • TanStack Query
  • SWR
  • las funcionalidades de obtención de datos de su framework
  • Componentes de servidor, donde sean aplicables

De lo contrario, tendrá que gestionar manualmente cada uno de estos aspectos para cada solicitud:

loading
error
data
refetch
cache
retry
stale data
race conditions

Una capa de datos se encarga de todo eso por usted. Conceptualmente, una consulta declara qué datos necesita y cómo obtenerlos, y devuelve el estado actual:

const { data, isLoading, error } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId)
});

La clave de la consulta también funciona como clave de caché, por lo que los componentes que solicitan al mismo usuario comparten una única solicitud, y al cambiar userId se obtiene el nuevo usuario sin que previas respuestas obsoletas interfieran. El componente vuelve a renderizar la interfaz de usuario en lugar de funcionar como una pequeña biblioteca de red.

Marcar actualizaciones no urgentes con useTransition

Imagínese un cuadro de búsqueda que gestiona una tabla costosa. Con una sola actualización de estado, cada tecla pulsada obliga a la tabla a volver a renderizarse antes de que la entrada pueda mostrar el siguiente carácter:

function handleChange(e) {
  setQuery(e.target.value);
}

La solución es dividir el valor en dos partes: lo que muestra la entrada y lo que utiliza la tabla para filtrar:

const [inputValue, setInputValue] = useState('');
const [query, setQuery] = useState('');

El valor de la entrada se actualiza normalmente, mientras que la actualización de la consulta se envuelve en startTransition:

const [isPending, startTransition] = useTransition();function handleChange(e) {
  const value = e.target.value;  setInputValue(value);  startTransition(() => {
    setQuery(value);
  });
}

React ahora trata estas dos actualizaciones de manera diferente. Escribir es algo urgente:

Typing
↓
Urgent

mientras que el filtrado y la re-renderización pueden interrumpirse y retrasarse:

Filtering/rendering
↓
Can wait

Una transición no hace que el código lento sea más rápido; simplemente cambia la programación para que la interfaz siga siendo receptiva y un nuevo tecleo pueda interrumpir el proceso de renderizado en curso. isPending permite indicar que la tabla está actualizándose.

Mida antes de memorizar

El error opuesto es la optimización por reflejo. A menudo comienza con un cálculo memorizado:

const result = useMemo(() => {
  return calculateSomething(data);
}, [data]);

seguido de una asignación memorizada:

const users = useMemo(() => {
  return data.map(...);
}, [data]);

luego una definición de columna memorizada:

const columns = useMemo(() => [...], []);

y opciones memorizadas:

const options = useMemo(() => [...], []);

Pronto, cada valor en el componente queda envuelto en una función de memoización, pero nadie verifica si algo es lento. useMemo ayuda con tareas costosas y proporciona referencias estables para los hijos memorizados, pero la optimización debe realizarse después de un bucle:

Problem
  ↓
Measure
  ↓
Find bottleneck
  ↓
Optimize
  ↓
Measure again

y no como atajo:

Component feels complicated
  ↓
Add useMemo

Para cálculos sencillos, la memorización solo añade carga adicional por lecturas y arrays de dependencias que pueden volverse obsoletos. El Profilador de React DevTools indica si un componente es realmente el cuello de botella.

Utilice las primitivas añadidas en React 19

Varias prácticas de versiones anteriores de React ahora tienen sustitutos más simples. Durante años, pasar un ref a su propio componente significaba envolverlo en forwardRef. En React 19, los componentes funcionales reciben ref como un prop ordinario:

function CustomInput({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

La interfaz de usuario optimista es otro ejemplo. En lugar de insertar manualmente un elemento temporal en el estado y recordar eliminarlo o reemplazarlo posteriormente:

setTodos(prev => [
  ...prev,
  {
    id: 'temp',
    text,
    pending: true
  }
]);

puede utilizar useOptimistic, que deriva una vista temporal a partir del estado real:

const [optimisticTodos, addOptimisticTodo] = useOptimistic(
  todos,
  (state, todo) => [...state, todo]
);

Se muestra el valor optimista mientras una acción asíncrona está en curso y se vuelve al verdadero todos una vez que finaliza, por lo que no hay ningún elemento temporal que deba eliminarse. Está diseñado para ser llamado dentro de una acción o transición; los casos límite relacionados con solicitudes fallidas se tratan en nuestro análisis de los modos de fallo de useOptimistic rollback.

Para IDs que vinculan etiquetas a campos de entrada con fines de accesibilidad, utilice el gancho integrado:

const id = useId();

en lugar de generar uno al azar:

const id = `input-${Math.random()}`;

Un ID aleatorio cambia en cada renderizado y difiere entre el servidor y el cliente, lo que rompe la hidratación y la asociación con las etiquetas. React sigue añadiendo primitivas que expresan directamente la intención, por lo que evite resolver problemas actuales con patrones anteriores a ellos.

Lista de verificación antes de agregar un hook

Conocer bien React implica saber cuándo no usar hooks. Un componente no mejora al utilizarlos todos:

useEffect()
useMemo()
useCallback()
useRef()
useState()

Busque un flujo de datos fácil de seguir. Antes de recurrir a un hook, nombre el problema y seleccione la herramienta adecuada.

Para datos derivados:

Calculate it.

Para una acción del usuario:

Handle the event.

Para sincronizarse con algo fuera de React, como una suscripción, un temporizador o un widget no basado en React:

Consider useEffect.

Para leer desde un almacenamiento externo:

Consider useSyncExternalStore or a state library.

Para una actualización importante pero no urgente:

Consider useTransition.

Para un cálculo que podría ser costoso:

Measure first, then consider useMemo.

Y cuando React ya ofrece una función primitiva para esa tarea:

Use the primitive instead of rebuilding it yourself.

Puntos clave

  • Los componentes más saludables suelen tener la menor cantidad de ganchos innecesarios, no la mayor.
  • Sustituya “¿Dónde puedo usar useEffect?” por “¿Realmente se necesita un Effect aquí?”. La mayoría de las veces la respuesta se refiere a cálculos en tiempo de renderizado, un manejador de eventos o una biblioteca de datos.
  • Aprenda el problema que resuelve cada nueva API de React antes de estudiar su sintaxis.
  • A veces, la optimización más efectiva en un componente es eliminar un gancho en lugar de agregar uno.