Si alguna vez ha utilizado la API fetch
de JavaScript para mejorar el envío de un formulario, es muy probable que haya introducido accidentalmente un error de solicitud duplicada/condición de carrera. Hoy, lo guiaré a través del problema y mis recomendaciones para evitarlo.
(Video al final si lo prefieres)
Consideremos una muy básica
<form method="post"> <label for="name">Name</label> <input id="name" name="name" /> <button>Submit</button> </form>
Cuando presionamos el botón de enviar, el navegador actualizará toda la página.
Observe cómo se recarga el navegador después de hacer clic en el botón Enviar.
La actualización de la página no siempre es la experiencia que queremos ofrecer a nuestros usuarios, por lo que una alternativa común es usarfetch
.
Un enfoque simplista podría parecerse al siguiente ejemplo.
Después de que se monte la página (o el componente), tomamos el nodo DOM del formulario, agregamos un detector de eventos que construye una solicitud fetch
usando el formulariopreventDefault()
del evento.
const form = document.querySelector('form'); form.addEventListener('submit', handleSubmit); function handleSubmit(event) { const form = event.currentTarget; fetch(form.action, { method: form.method, body: new FormData(form) }); event.preventDefault(); }
Ahora, antes de que cualquier hotshot de JavaScript comience a twittearme sobre GET vs. POST y solicite el cuerpo yfetch
deliberadamente simple porque ese no es el enfoque principal.
El problema clave aquí es event.preventDefault()
. Este método evita que el navegador realice el comportamiento predeterminado de cargar la nueva página y enviar el formulario.
Ahora, si miramos la pantalla y presionamos enviar, podemos ver que la página no se vuelve a cargar, pero sí vemos la solicitud HTTP en nuestra pestaña de red.
Tenga en cuenta que el navegador no recarga la página completa.
Desafortunadamente, al usar JavaScript para evitar el comportamiento predeterminado, en realidad hemos introducido un error que el comportamiento predeterminado del navegador no tiene.
Cuando usamos simple
Si lo comparamos con el ejemplo de JavaScript, veremos que todas las solicitudes se envían y todas están completas sin que se cancele ninguna.
Esto puede ser un problema porque, aunque cada solicitud puede tomar una cantidad de tiempo diferente, podrían resolverse en un orden diferente al que se iniciaron. Esto significa que si agregamos funcionalidad a la resolución de esas solicitudes, es posible que tengamos algún comportamiento inesperado.
Como ejemplo, podríamos crear una variable para incrementar por cada solicitud (" totalRequestCount
"). Cada vez que ejecutamos la función handleSubmit
, podemos incrementar el recuento total y capturar el número actual para realizar un seguimiento de la solicitud actual (" thisRequestNumber
").
Cuando se resuelve una solicitud fetch
, podemos registrar su número correspondiente en la consola.
const form = document.querySelector('form'); form.addEventListener('submit', handleSubmit); let totalRequestCount = 0 function handleSubmit(event) { totalRequestCount += 1 const thisRequestNumber = totalRequestCount const form = event.currentTarget; fetch(form.action, { method: form.method, body: new FormData(form) }).then(() => { console.log(thisRequestNumber) }) event.preventDefault(); }
Ahora, si presionamos el botón Enviar varias veces, es posible que veamos diferentes números impresos en la consola fuera de orden: 2, 3, 1, 4, 5. Depende de la velocidad de la red, pero creo que todos podemos estar de acuerdo. que esto no es lo ideal.
Considere un escenario en el que un usuario activa varias solicitudes fetch
en una sucesión cercana y, al finalizar, su aplicación actualiza la página con sus cambios. En última instancia, el usuario podría ver información inexacta debido a que las solicitudes se resolvieron fuera de orden.
Esto no es un problema en el mundo sin JavaScript porque el navegador cancela cualquier solicitud anterior y carga la página después de que se completa la solicitud más reciente, cargando la versión más actualizada. Pero las actualizaciones de página no son tan atractivas.
La buena noticia para los amantes de JavaScript es que podemos tener un
Sólo tenemos que hacer un poco más de trabajo preliminar.
Si observa la documentación de la API fetch
, verá que es posible cancelar una recuperación utilizando un AbortController
y la propiedad signal
de las opciones fetch
. Se ve algo como esto:
const controller = new AbortController(); fetch(url, { signal: controller.signal });
Al proporcionar la señal de AbortContoller
a la solicitud fetch
, podemos cancelar la solicitud en cualquier momento en que se active el método abort
de AbortContoller
.
Puede ver un ejemplo más claro en la consola de JavaScript. Intente crear un AbortController
, inicie la solicitud fetch
y luego ejecute inmediatamente el método abort
.
const controller = new AbortController(); fetch('', { signal: controller.signal }); controller.abort()
Debería ver inmediatamente una excepción impresa en la consola. En los navegadores Chromium, debería decir: "DOMException no detectada (en promesa): el usuario canceló una solicitud". Y si explora la pestaña Red, debería ver una solicitud fallida con el texto de estado "(cancelado)".
Con eso en mente, podemos agregar un AbortController
al controlador de envío de nuestro formulario. La lógica será la siguiente:
AbortController
para cualquier solicitud anterior. Si existe uno, cancelarlo.
AbortController
para la solicitud actual que se puede cancelar en solicitudes posteriores.
AbortController
correspondiente.
Hay varias formas de hacer esto, pero usaré un WeakMap
para almacenar las relaciones entre cada nodo DOM <form>
enviado y su respectivo AbortController
. Cuando se envía un formulario, podemos verificar y actualizar WeakMap
en consecuencia.
const pendingForms = new WeakMap(); function handleSubmit(event) { const form = event.currentTarget; const previousController = pendingForms.get(form); if (previousController) { previousController.abort(); } const controller = new AbortController(); pendingForms.set(form, controller); fetch(form.action, { method: form.method, body: new FormData(form), signal: controller.signal, }).then(() => { pendingForms.delete(form); }); event.preventDefault(); } const forms = document.querySelectorAll('form'); for (const form of forms) { form.addEventListener('submit', handleSubmit); }
La clave es poder asociar un controlador de aborto con su formulario correspondiente. Usar el nodo DOM del formulario como clave de WeakMap
es una forma conveniente de hacerlo.
Con eso en su lugar, podemos agregar la señal de AbortController
a la solicitud fetch
, abortar cualquier controlador anterior, agregar otros nuevos y eliminarlos al finalizar.
Con suerte, todo eso tiene sentido.
Ahora, si presionamos el botón de envío de ese formulario varias veces, podemos ver que todas las solicitudes de API, excepto la más reciente, se cancelan.
Esto significa que cualquier función que responda a esa respuesta HTTP se comportará más como cabría esperar.
Ahora, si usamos la misma lógica de conteo y registro que tenemos arriba, podemos presionar el botón Enviar siete veces y veríamos seis excepciones (debido al AbortController
) y un registro de "7" en la consola.
Si volvemos a enviar y damos suficiente tiempo para que se resuelva la solicitud, veremos "8" en la consola. Y si presionamos el botón Enviar varias veces, nuevamente, continuaremos viendo las excepciones y el recuento final de solicitudes en el orden correcto.
Si desea agregar más lógica para evitar ver DOMExceptions en la consola cuando se cancela una solicitud, puede agregar un bloque .catch()
después de su solicitud fetch
y verificar si el nombre del error coincide con " AbortError
":
fetch(url, { signal: controller.signal, }).catch((error) => { // If the request was aborted, do nothing if (error.name === 'AbortError') return; // Otherwise, handle the error here or throw it back to the console throw error });
Toda esta publicación se centró en los formularios mejorados con JavaScript, pero probablemente sea una buena idea incluir un AbortController
cada vez que cree una solicitud fetch
. Realmente es una lástima que no esté integrado en la API. Pero con suerte, esto le muestra un buen método para incluirlo.
También vale la pena mencionar que este enfoque no evita que el usuario envíe spam al botón de envío varias veces. Todavía se puede hacer clic en el botón y la solicitud aún se dispara, solo proporciona una forma más consistente de manejar las respuestas.
Desafortunadamente, si un usuario envía spam a un botón de envío, esas solicitudes aún irían a su backend y podrían consumir una gran cantidad de recursos innecesarios.
Algunas soluciones ingenuas pueden ser deshabilitar el botón de enviar, usando un
No abordan el abuso a través de solicitudes programadas.
Para abordar el abuso de demasiadas solicitudes a su servidor, probablemente desee configurar algunos
También vale la pena mencionar que la limitación de velocidad no resuelve el problema original de solicitudes duplicadas, condiciones de carrera y actualizaciones de IU inconsistentes. Lo ideal es que utilicemos ambos para cubrir ambos extremos.
De todos modos, eso es todo lo que tengo por hoy. Si quieres ver un video que trata sobre este mismo tema, mira esto.
Muchas Gracias Por Leer. Si te ha gustado este artículo, por favor
Publicado originalmente en