Cancelando promesas en React

Si estás trabajando con React en algún momento puede haberte llegado a la consola del navegador un aviso sobre una pérdida de memoria al haber actualizado el estado de un componente estando desmontado:

image.png

Muchos de nosotros evitamos este problema recurriendo a la típica comprobación de isMounted, donde básicamente controlábamos el desmonte de un componente de React mediante una referencia que consultábamos justo antes de hacer la actualización del estado.

Recordemos que en las primeras versiones de React teníamos esta propiedad de forma interna en la librería, pero fue deprecada allá por el 2015 porque se trataba de un anti-patrón... una mala práctica que el equipo de React avisaba y desaconsejaba su uso. Eso sí, al menos nos mostraban el camino correcto 😅

Ahora bien, ¿qué podemos hacer para solucionar este problema?

Podemos continuar con el uso del isMounted, pero como bien nos recomienda el artículo, lo suyo sería cancelar la promesa para que no se llegue a producir esa actualización del estado.

Pero si bien la forma anterior está correcta, no es lo más óptimo. Si nos fijamos en lo que sucede en nuestro navegador, vemos cómo efectivamente no tenemos esa pérdida de memoria porque no se intenta actualizar el estado cuando el componente está desmontado, pero lo que sí podemos ver es que nuestra llamada a la API continua su curso, descargando la información de la llamada aunque no haga nada con su respuesta.

Hemos solucionado un problema pero seguimos arrastrando una pérdida de recursos.

AbortController con Fetch

Hoy te propongo hacer uso de la interface AbortController que te permitirá abortar o cancelar una solicitud web cuando quieras.

Su uso es muy sencillo como verás a continuación:

Creamos el controlador

const controller = new AbortController();

Este objeto nos proporciona un método abort que será el que nos permitirá abortar nuestra solicitud web cuando deseemos. También nos retorna una propiedad signal que será la que le pasaremos a nuestra configuración fetch para que enlace dicha funcionalidad.

Cuando abort() sea llamado:

  • controller.signal emitirá un evento abort.
  • controller.signal.aborted será true.

Cómo lo usamos

const controller = new AbortController();
fetch(url, {
  signal: controller.signal
});

Para nuestra suerte, fetch permite el uso de la interface AbortController así que con el código anterior estará preparado para actuar en caso de que nuestra señal sea abortada.

Cancelando una llamada

Ahora para cancelar nuestra llamada a la API, sólo debemos ejecutar el método abort de nuestro objeto controller:

controller.abort();

Ahora veremos cómo podemos crearnos un hook para encapsular esta funcionalidad 👇

useAbortableSignal Hook

export const useAbortableSignal = () => {
  const [controller] = useState(() => new AbortController());

  useEffect(() => {
    return () => {
      controller.abort();
    };
  }, []);

  return {
    signal: controller.signal
  };
};

Su uso en un componente

export const useData = () => {
  const { signal } = useAbortableSignal();

  const [isLoading, setIsLoading] = React.useState(true);
  const [error, setError] = React.useState('');
  const [data, setData] = React.useState([]);

  React.useEffect(() => {
    const fetchData = async () => {
      try {
        const result = await getData(signal);
        setData(result);
      } catch (err) {
        if (err) {
          const isAborted = err.name === 'AbortError';
          if (isAborted) {
            return;
          }

          setError('Something went wrong while fetching data');
        }
      } finally {
        setIsLoading(false);
      }
    };

    fetchData();
  }, []);

  return { isLoading, data, error };
};

Con el código anterior además estás controlando que el error manejado en el catch sea porque la solicitud ha sido cancelada y lo podríamos omitir sin problema.

Y ahora si ves el resultado en la pestaña de red de las herramientas de desarrollador... 😊

image.png

¿Puedo usar el mismo controlador para varias promesas?

Sí, y de hecho si lo que quieres es cancelar la ejecución de las llamadas a la API en un componente, tiene sentido que todas las solicitudes compartan el mismo controlador porque cuando se desmonte el componente, abortaremos el controlador cancelando todas las solicitudes vivas en el componente.

Compatibilidad

image.png

En el momento de haber escrito este artículo, la tabla de compatibilidad era la de la imagen, pero te aconsejo que la revises nuevamente en este enlace si tienes alguna duda.

Código de ejemplo y comparativa

Ver completo: codesandbox.io/s/abortable-fetch-ffkyzh

Recursos