Cancelación de trabajos obsoletos en React: AbortController para el cambio de seguimiento
Aprenda por qué las pistas omitidas y las consultas obsoletas siguen escribiendo en su interfaz de usuario, cómo AbortController cancela la solicitud real y cómo complementa a debounce y throttle en React.
Cuando un usuario cambia de opinión más rápido de lo que la red responde, cualquier solicitud que ya haya iniciado sigue ejecutándose y escribirá felizmente su resultado en la pantalla. En un cuadro de búsqueda, eso significa resultados de una consulta que el usuario abandonó; en un reproductor de medios, significa un breve reproducimiento de la canción que acaba de saltarse. Esperar o aplicar límites de velocidad no resuelve este problema, ya que el trabajo ya está en curso. Este artículo muestra cómo AbortController cancela realmente ese trabajo, cómo integrarlo en un efecto de React y cómo funciona junto con debounce y throttle en lugar de reemplazarlos.
La carrera: las respuestas llegan en el orden incorrecto
Considere un campo de búsqueda. El usuario escribe “ni” y se envía una solicitud. Luego escribe “ke”, y se envía una segunda solicitud para “nike”. Si el servidor responde más lentamente a la primera consulta, más corta, su respuesta llega después de la segunda y reemplaza los resultados correctos por otros obsoletos.
Un reproductor de audio sigue el mismo patrón, pero con consecuencias mayores. Al tocar una pista se inicia su carga. Al tocar otra pista antes de que la primera esté lista, se inicia una segunda carga, y ahora ambas compiten. Por un momento, el oyente podría escuchar la pista que se saltó, la barra de progreso podría mostrar una duración incorrecta, o ambas pistas podrían intentar tomar el control del reproductor.
El mecanismo de amortiguación no ayudaría aquí. Este sistema retrasa el inicio del proceso hasta que cesa la entrada, pero no hace nada respecto a una solicitud que ya está en ejecución. Lo que falta es la capacidad de cancelación: indicarle al proceso ya iniciado que se detenga.
Qué sale mal sin cancelación
fetch, las transmisiones en flujo, la carga de medios y los listeners de eventos representan tareas que continúan por sí solas una vez iniciadas. Al no haber forma de detenerlas, suelen producirse tres tipos de problemas:
- Los datos obsoletos prevalecen. Una respuesta más antigua se resuelve después de una nueva y reemplaza lo que se muestra en la pantalla o lo que comienza a reproducirse en un reproductor.
- Recursos desperdiciados. Ancho de banda, batería, CPU del servidor y solicitudes a CDN se gastan en contenido que nadie verá ni escuchará.
- Una interfaz afectada. Indicadores de carga que nunca se detienen, una forma de onda correspondiente a la canción anterior, un título que cambia dos veces seguidas.
La solución clásica es utilizar una condición de protección como if (requestId !== latestId) return dentro de cada callback, o ignorar silenciosamente el resultado en un .then(). Esto solo funciona si cada callback recuerda realizar esa verificación, y la solicitud se completa y descarga sus datos. AbortController va un paso más allá al cancelar la operación en sí, en lugar de solo suspender el interés en su resultado.
El patrón básico de AbortController
Un controlador expone un signal. Pasa ese signal a cualquier API que lo acepte, y al llamar a abort() en el controlador se indica a todos los que poseen ese signal que dejen de funcionar:
const controller = new AbortController();
fetch("/api/tracks/123", { signal: controller.signal })
.then((res) => res.json())
.then((track) => loadIntoPlayer(track))
.catch((err) => {
if (err.name === "AbortError") {
// expected. they picked a different song.
return;
}
throw err;
});
// they skipped, or left the page, or closed the player
controller.abort();
Tres detalles merecen atención. Primero, la señal se pasa en las opciones de fetch, que es cómo fetch sabe que puede ser cancelada. Segundo, al abortar la promesa se rechaza con un error denominado AbortError, incluso si los encabezados ya han llegado y res.json() sigue leyendo el cuerpo. Tercero, el bloque catch trata ese error como una salida normal y esperada y vuelve a lanzar todo lo demás, por lo que los fallos reales no se ignoran.
Toda esta técnica se basa en un ciclo de vida simple: un controlador por unidad de intención. Cuando el usuario elige una nueva pista, se aborta el controlador anterior y se crea uno nuevo para la nueva carga. Un controlador no puede reiniciarse después de haberse abortado, por lo que reutilizarlo en distintas solicitudes cancelaría inmediatamente el trabajo futuro.
Si se llama a abort(reason) con una razón personalizada, fetch rechaza la solicitud con esa razón en lugar del valor predeterminado AbortError. En ese caso, verificar controller.signal.aborted es una forma más fiable de distinguir entre una cancelación intencionada y un fallo real.
Vincular la cancelación a un efecto de React
En React, el lugar natural para realizar la llamada de aborto es en la limpieza del efecto. Cuando el valor que impulsa una solicitud proviene de props o estado, la solicitud debe mantenerse activa exactamente tanto tiempo como ese valor:
useEffect(() => {
const controller = new AbortController();
fetch(`/api/tracks/${trackId}`, { signal: controller.signal })
.then((res) => res.json())
.then(setTrack)
.catch((err) => {
if (err.name === "AbortError") return;
setError(err);
});
return () => controller.abort();
}, [trackId]);
Cuando trackId cambia, React ejecuta la limpieza del renderizado anterior antes de iniciar el siguiente efecto, por lo que la solicitud en curso para la pista antigua se cancela y solo entonces comienza la nueva. Cuando el reproductor se desmonta, se ejecuta la misma limpieza, lo que significa que una respuesta tardía nunca podrá llamar a setTrack en un componente que ya no existe.
En el modo de desarrollo con Modo Estricto, React monta, limpia y vuelve a ejecutar los efectos una vez intencionadamente. Con este patrón verás una solicitud cancelada en el panel de red; esa es la limpieza realizando su función, y la protección contra AbortError impide que aparezca como un error.
La misma señal puede controlar más de un fetch. Los flujos la aceptan, así como las bibliotecas que envuelven XHR, y addEventListener acepta una opción signal que elimina al oyente cuando la señal se interrumpe. Una precaución para los reproductores: un elemento <audio> simple no acepta señales. Para evitar que almacene en búfer un archivo omitido, también hay que eliminar o reemplazar su src durante la limpieza.
Debounce, throttle y abort resuelven problemas diferentes
Estas tres herramientas suelen aparecer juntas alrededor de los campos de búsqueda y los controles de reproductor, por lo que es fácil confundirlas. Cada una actúa en un momento distinto:
- Debounce espera a que el usuario haga una pausa y luego realiza la acción una sola vez. Escribir rápidamente “n-i-k-e” podría generar una única solicitud después de la última tecla pulsada.
Dicho de otra manera, debounce y throttle deciden cuándo se permite iniciar un nuevo trabajo, mientras que abort decide si el trabajo ya iniciado puede continuar.
Un cuadro de búsqueda que parece reactivo suele utilizar dos de ellos. Debounce evita enviar una solicitud por cada tecla pulsada, y abort asegura que una solicitud ya enviada no pueda regresar más tarde y sobrescribir resultados más recientes. El artículo dedicado a las condiciones de carrera que debounce no puede resolver en las interfaces de búsqueda analiza ese caso en profundidad.
Un reproductor sigue el mismo patrón. Puedes limitar la frecuencia de uso del botón “siguiente” para que un usuario impaciente no pueda iniciar veinte cargas en 200 ms, pero esa limitación no cancela nada; solo espacia las nuevas solicitudes. Aún así es necesario abortar la carga que ya comenzó.
Cada uno de estos métodos por separado deja un vacío: la función de debounce sigue permitiendo que una solicitud inicial lenta llegue tarde, y el aborto por sí solo sigue sobrecargando al servidor.
Análisis de una competencia en lista de reproducción
Considera lo que ocurre en un reproductor de audio cuando alguien navega por una lista de reproducción más rápido de lo que la red puede procesar.
El usuario toca la pista A. La aplicación solicita metadatos, quizás una URL firmada del archivo, la portada y una forma de onda, y el audio comienza a cargarse en memoria intermedia. Antes de que todo eso termine, el usuario toca la pista B y luego la pista C.
Sin posibilidad de cancelación, todo lo iniciado para la pista A sigue llegando:
- Llegan los metadatos de la pista A y se establece su título.
Con la opción de cancelación, tocar B interrumpe todo lo que se emitió en nombre de A. Las únicas respuestas permitidas para modificar el elemento de audio, el título y la forma de onda corresponden a la pista seleccionada en ese momento. Al tocar “Saltar” nuevamente, la acción de B se cancela mientras C continúa. Al cerrar el reproductor, se ejecuta la limpieza correspondiente y ya no hay nada que escriba en un componente que ya no existe.
Compartir un controlador entre todas las solicitudes de selección, y un único abort() detiene todo el grupo.
El efecto neto es que la interfaz de usuario solo refleja la intención actual del usuario.
El mismo patrón en aplicaciones más grandes
Las aplicaciones grandes aplican esta idea en todas partes:
- Typeahead y filtros. Una nueva consulta cancela la solicitud anterior, lo que equivale a una competencia por la lista de reproducción entre los resultados de búsqueda en lugar de las canciones.
- Ruteo del lado del cliente. Cuando un usuario abandona una página antes de que se devuelvan sus datos, frameworks y bibliotecas de datos como Next.js, Remix, TanStack Query y SWR pueden abortar el trabajo pendiente durante la navegación; en esencia, sigue siendo una señal de aborto. Consulte la documentación de cada biblioteca para saber exactamente cuándo cancela y si envía una señal a su mecanismo de solicitud.
controller.abort() que se ejecutan detrás de ese botón.Puntos clave
- El debounce y el throttle controlan cuándo comienza el proceso; solo la cancelación determina si el trabajo iniciado se finaliza.
- Cree un
AbortControllerpor cada tarea, pase susignala todos los lugares donde se ejecute el trabajo y cámbielo en lugar de reutilizarlo. - Trate
AbortErrorcomo una salida normal y vuelva a lanzar los demás errores. - En React, aborta en la limpieza del efecto para que los cambios en los campos de entrada o el desmontaje cancelemos automáticamente las solicitudes pendientes.
- Recuerda qué no alcanza una señal, como la carga propia de un elemento multimedia, y limpia esos elementos de forma explícita.
La cancelación por sí sola no hará que una interfaz sea más inteligente, pero garantiza que la solicitud de ayer no pueda interferir con la de hoy. Una vez hayas visto cómo se desarrolla ese conflicto en la pantalla o a través de un altavoz, establecer una señal para cada solicitud que tenga permiso para actualizar la interfaz se convierte en un hábito valioso de mantener.
Lecturas relacionadas
- UI optimista sin desincronizaciones: snapshots, retrocesos y solicitudes anuladas — Aprenda a crear actualizaciones optimistas que mantengan la corrección: estado de snapshot, actualización instantánea, retroceso en caso de fallo, cancelación de solicitudes obsoletas y cómo saber cuándo no utilizar este patrón.
- Arreglando condiciones de carrera: el retardo no puede solucionarlo en interfaces de usuario de búsqueda — Entienda por qué el retardo por sí solo no puede evitar que las respuestas API obsoletas sobrescriban el estado actual de la interfaz, y explore cuatro soluciones prácticas para garantizar el orden de las solicitudes.