_

Software Developer

Optimización del manejo de formularios con useTransition en lugar de useState

12 de noviembre de 2024Tiempo de lectura estimado7 minutos

En el desarrollo de React, gestionar estados de carga con valores booleanos como [isLoading, setIsLoading] es un patrón común, especialmente para formularios y otras acciones asíncronas. Aunque este enfoque funciona, a medida que las aplicaciones crecen en complejidad, usar useState puede impactar negativamente el rendimiento y la experiencia de usuario. Afortunadamente, React ofrece herramientas como useTransition para gestionar transiciones de UI de forma más eficiente.

El enfoque típico con useState

Aquí tienes un ejemplo común de cómo gestionar un estado de carga en un formulario usando useState:

import { useState, FormEvent } from "react";

function ContactForm() {
  const [isLoading, setIsLoading] = useState<boolean>(false);

  const handleSubmit = async (event: FormEvent) => {
    event.preventDefault();
    setIsLoading(true);

    // Simulamos el envío de un formulario
    await fakeApiRequest();

    setIsLoading(false);
    alert("Formulario enviado con éxito");
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" placeholder="Tu nombre" required />
      <button type="submit" disabled={isLoading}>
        {isLoading ? "Enviando..." : "Enviar"}
      </button>
    </form>
  );
}

function fakeApiRequest(): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, 2000));
}

export default ContactForm;

En este caso, usamos useState para deshabilitar el botón de envío mientras esperamos la respuesta de nuestra "API". Aunque este enfoque funciona, no es ideal en situaciones donde el rendimiento y la respuesta rápida de la UI son críticos.

Problemáticas de useState

Problemas de rendimiento

Cuando usamos useState para controlar un estado de carga como isLoading, cada vez que actualizamos ese estado, React vuelve a renderizar el componente. Esto está bien en casos simples, pero las actualizaciones frecuentes o la lógica más compleja pueden generar problemas de rendimiento. Al cambiar un valor booleano como isLoading, la UI puede congelarse momentáneamente, impidiendo que el usuario interactúe hasta que el estado cambie nuevamente.

Esto sucede porque React desencadena una re-renderización del componente al detectar un cambio de estado con useState. En aplicaciones más grandes con múltiples componentes, esto puede volverse innecesario o incluso costoso, afectando la velocidad y la percepción del usuario, que puede sentir que la app es lenta o poco receptiva.

Problemas de sincronización del estado

Además del rendimiento, useState puede generar problemas de sincronización, especialmente cuando varios componentes o hooks dependen del mismo estado. Si no se gestiona correctamente, puede haber situaciones donde la UI muestre un estado inconsistente, especialmente con eventos asíncronos o cambios rápidos de estado.

Manejar manualmente las actualizaciones del estado se vuelve más complicado a medida que la app crece, llevando a una base de código más compleja y con posibles errores de sincronización. Aquí es donde useTransition resulta valioso para controlar transiciones sin bloquear la UI.

¿Qué es useTransition?

useTransition es un hook que te permite marcar una actualización de estado como "transicional", lo que significa que React puede priorizar las actualizaciones más urgentes (como animaciones o interacciones del usuario) y manejar las transiciones cuando sea más conveniente.

La ventaja clave de useTransition es que mejora el rendimiento percibido de la app, permitiendo que React gestione actualizaciones de manera más inteligente, ofreciendo transiciones más suaves y mejores experiencias de usuario.

Ventajas de usar useTransition

  1. Mejor rendimiento percibido: useTransition permite que React retrase las actualizaciones de estado no urgentes, lo que mejora la sensación de rapidez en la aplicación.
  2. Transiciones más suaves: En vez de bloquear la UI inmediatamente (como cuando usamos useState para controlar isLoading), useTransition permite que el usuario siga interactuando con la aplicación mientras la operación de fondo se está completando.
  3. Menos código y más declarativo: Al usar useTransition, nos ahorramos la necesidad de escribir lógica adicional para gestionar el estado de carga. React se encarga de marcar las actualizaciones como transicionales automáticamente.
  4. Flexibilidad en grandes aplicaciones: Si tu aplicación tiene múltiples operaciones asíncronas que podrían afectar el rendimiento, useTransition ayuda a controlar mejor esas transiciones, haciendo que la UI no se vea afectada por operaciones no urgentes.

Reemplazando useState por useTransition

Ahora, vamos a ver cómo sería nuestro formulario utilizando useTransition para manejar el estado de "cargando". En lugar de bloquear la UI con setIsLoading(true), haremos que el envío del formulario sea una transición suave que no interrumpa las interacciones del usuario.

import { useState, useTransition, FormEvent, ChangeEvent } from 'react';

interface FormData {
  name: string;
}

function ContactForm() {
  const [isPending, startTransition] = useTransition();
  const [formData, setFormData] = useState<FormData>({ name: '' });

  const handleSubmit = (event: FormEvent) => {
    event.preventDefault();

    startTransition(async () => {
      // Simulamos el envío de un formulario
      await fakeApiRequest();
      
      alert("Formulario enviado con éxito");
    });
  };

  const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { name, value } = event.target;
    setFormData((prev) => ({ ...prev, [name]: value }));
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="name"
        value={formData.name}
        onChange={handleInputChange}
        placeholder="Tu nombre"
        required
      />
      <button type="submit" disabled={isPending}>
        {isPending ? "Enviando..." : "Enviar"}
      </button>
    </form>
  );
}

function fakeApiRequest(): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, 2000));
}

export default ContactForm;

En este nuevo ejemplo, hemos eliminado completamente la necesidad de manejar un estado isLoading manualmente. En su lugar, utilizamos el booleano isPending, que nos proporciona useTransition, para saber si estamos en medio de una transición o no. El código es más limpio y dejamos que React gestione de manera más eficiente cuándo y cómo actualizar el estado del componente.

Conclusión

Cambiar de useState a useTransition para manejar estados como isLoading puede parecer un pequeño ajuste, pero en aplicaciones más grandes, este enfoque puede marcar una gran diferencia en términos de rendimiento y experiencia de usuario. El manejo de las transiciones se vuelve mucho más fluido y React optimiza de manera inteligente las actualizaciones de UI.