paint-brush
Una guía sobre cómo cancelar solicitudes de recuperación duplicadas en formularios mejorados de JavaScriptpor@austingil
2,459 lecturas
2,459 lecturas

Una guía sobre cómo cancelar solicitudes de recuperación duplicadas en formularios mejorados de JavaScript

por Austin Gil8m2023/02/10
Read on Terminal Reader

Demasiado Largo; Para Leer

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. 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.
featured image - Una guía sobre cómo cancelar solicitudes de recuperación duplicadas en formularios mejorados de JavaScript
Austin Gil HackerNoon profile picture

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 formulario HTML con una sola entrada y un botón de envío.


 <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 usar JavaScript para agregar un detector de eventos al evento "enviar" del formulario, evitar el comportamiento predeterminado y enviar los datos del formulario mediante la API fetch .


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 formulario acción , método , y datos , y al final del controlador, llamamos al método preventDefault() 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 y Tipo de contenido y cualquier otra cosa, déjame decirte, lo sé. Mantengo la solicitud fetch 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 HTML y presiona el botón Enviar un montón de veces muy rápido, notará que todas las solicitudes de la red, excepto la más reciente, se vuelven rojas. Esto indica que se cancelaron y solo se acepta la solicitud más reciente.


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 experiencia de usuario atractiva ¡Y una interfaz de usuario consistente!


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)".


Las herramientas de desarrollo de Chrome se abrieron a la red con la consola de JavaScript abierta. En la consola está el código "const controller = new AbortController();fetch('', { signal: controller.signal });controller.abort()", seguido de la excepción, "Uncaught (en promesa) DOMException: The el usuario canceló una solicitud". En la red, hay una solicitud a "localhost" 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:


  • Primero, busque un AbortController para cualquier solicitud anterior. Si existe uno, cancelarlo.


  • A continuación, cree un AbortController para la solicitud actual que se puede cancelar en solicitudes posteriores.


  • Finalmente, cuando se resuelve una solicitud, elimine su 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 });

Clausura

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 rebote , o solo crear nuevas solicitudes después de que se resuelvan las anteriores. No me gustan estas opciones porque se basan en ralentizar la experiencia del usuario y solo funcionan en el lado del cliente.


No abordan el abuso a través de solicitudes programadas.


Para abordar el abuso de demasiadas solicitudes a su servidor, probablemente desee configurar algunos limitación de velocidad . Eso va más allá del alcance de esta publicación, pero vale la pena mencionarlo.


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 Compártelo . Es una de las mejores formas de apoyarme. Tú también puedes Inscríbete a mi boletín de noticias o Sigueme en Twitter si quieres saber cuando se publican nuevos artículos.


Publicado originalmente en austingil.com .