Si vous avez déjà utilisé JavaScript fetch
API pour améliorer la soumission d’un formulaire, il y a de fortes chances que vous ayez accidentellement introduit un bogue de demande en double/condition de concurrence. Aujourd’hui, je vais vous expliquer le problème et mes recommandations pour l’éviter. (Il y a une vidéo à la fin si vous préférez cela.)
Considérons un formulaire HTML très basique avec une seule entrée et un bouton d’envoi.
<form method="post"> <label for="name">Name</label> <input id="name" name="name" /> <button>Submit</button> </form>

Lorsque nous appuyons sur le bouton Soumettre, le navigateur effectue une actualisation complète de la page.

Remarquez comment le navigateur se recharge après avoir cliqué sur le bouton Soumettre.
L’actualisation de la page n’est pas toujours l’expérience que nous souhaitons offrir à nos utilisateurs. Une alternative courante consiste donc à utiliser JavaScript pour ajouter un écouteur d’événement à l’événement « soumettre » du formulaire, empêcher le comportement par défaut et soumettre les données du formulaire à l’aide de la fetch
API.
Une approche simpliste pourrait ressembler à l’exemple ci-dessous. Après le montage de la page (ou du composant), nous saisissons le nœud DOM du formulaire, ajoutons un écouteur d’événement qui construit un fetch
request en utilisant l’action, la méthode et les données du formulaire, et à la fin du gestionnaire, nous appelons l’événement preventDefault()
méthode.
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(); }
Maintenant, avant que les hotshots JavaScript ne commencent à me tweeter à propos de GET vs POST et du corps de la demande et du type de contenu et de tout le reste, permettez-moi de dire, je sais. je garde le fetch
demande délibérément simple car ce n’est pas l’objectif principal.
La question clé ici est la event.preventDefault()
. Cette méthode empêche le navigateur d’exécuter le comportement par défaut de chargement de la nouvelle page et de soumission du formulaire.
Maintenant, si nous regardons l’écran et que nous appuyons sur soumettre, nous pouvons voir que la page ne se recharge pas, mais nous voyons la requête HTTP dans notre onglet réseau.

Notez que le navigateur n’effectue pas de rechargement complet de la page.
Malheureusement, en utilisant JavaScript pour empêcher le comportement par défaut, nous avons en fait introduit un bogue que le comportement par défaut du navigateur n’a pas.
Lorsque nous utilisons du HTML brut et que vous appuyez plusieurs fois très rapidement sur le bouton Soumettre, vous remarquerez que toutes les requêtes réseau, à l’exception de la plus récente, deviennent rouges. Cela indique qu’ils ont été annulés et que seule la demande la plus récente est honorée.

Si nous comparons cela à l’exemple JavaScript, nous verrons que toutes les demandes sont envoyées et toutes se terminent sans qu’aucune ne soit annulée.

Cela peut être un problème, car même si chaque demande peut prendre un temps différent, elles peuvent être résolues dans un ordre différent de celui dans lequel elles ont été lancées. Cela signifie que si nous ajoutons des fonctionnalités à la résolution de ces demandes, nous pourrions avoir un comportement inattendu.
Par exemple, nous pourrions créer une variable à incrémenter pour chaque requête (totalRequestCount
). Chaque fois que nous exécutons le handleSubmit
fonction, nous pouvons incrémenter le nombre total ainsi que capturer le nombre actuel pour suivre la demande actuelle (thisRequestNumber
). Lorsqu’un fetch
demande résolue, nous pouvons enregistrer son numéro correspondant dans la console.
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(); }
Maintenant, si nous frappons ce bouton de soumission plusieurs fois, nous pourrions voir différents numéros imprimés sur la console dans le désordre : 2, 3, 1, 4, 5. Cela dépend de la vitesse du réseau, mais je pense que nous pouvons tous être d’accord que ce n’est pas idéal.
Considérez un scénario où un utilisateur déclenche plusieurs fetch
requêtes en succession rapprochée et une fois terminées, votre application met à jour la page avec leurs modifications. L’utilisateur pourrait finalement voir des informations inexactes en raison de demandes résolues dans le désordre.
Il ne s’agit pas d’un problème dans le monde non-JavaScript, car le navigateur annule toute requête précédente et charge la page une fois la requête la plus récente terminée, en chargeant la version la plus récente. Mais les rafraîchissements de page ne sont pas aussi sexy.
La bonne nouvelle pour les amateurs de JavaScript est que nous pouvons avoir à la fois une expérience utilisateur sexy ET une interface utilisateur cohérente !
Nous avons juste besoin de faire un peu plus de démarches.
Si vous regardez le fetch
documentation de l’API, vous verrez qu’il est possible d’abandonner une récupération à l’aide d’un AbortController
et le signal
propriété de la fetch
options. Cela ressemble à ceci :
const controller = new AbortController();
fetch(url, { signal: controller.signal });
En fournissant le AbortContoller
signal au fetch
demande, nous pouvons annuler la demande à tout moment AbortContoller
c’est abort
méthode est déclenchée.
Vous pouvez voir un exemple plus clair dans la console JavaScript. Essayez de créer un AbortController
initiant la fetch
demande, puis exécutez immédiatement la abort
méthode.
const controller = new AbortController();
fetch('', { signal: controller.signal });
controller.abort()
Vous devriez immédiatement voir une exception imprimée sur la console. Dans les navigateurs Chromium, il devrait dire, « Uncaught (in promise) DOMException: L’utilisateur a abandonné une demande. » Et si vous explorez l’onglet Réseau, vous devriez voir une demande ayant échoué avec le texte d’état « (annulé) ».
Dans cet esprit, nous pouvons ajouter un AbortController
au gestionnaire de soumission de notre formulaire. La logique sera la suivante :
- Tout d’abord, recherchez un
AbortController
pour toute demande antérieure. S’il en existe un, annulez-le. - Ensuite, créez un
AbortController
pour la requête en cours qui peut être abandonnée sur les requêtes suivantes. - Enfin, lorsqu’une requête est résolue, supprimez son correspondant
AbortController
.
Il y a plusieurs façons de le faire, mais j’utiliserai un WeakMap
pour stocker les relations entre chaque soumission <form>
nœud DOM et ses nœuds respectifs AbortController
. Lorsqu’un formulaire est soumis, nous pouvons vérifier et mettre à jour WeakMap
par conséquent.
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); }
L’essentiel est de pouvoir associer un contrôleur d’abandon à son formulaire correspondant. Utiliser le nœud DOM du formulaire comme WeakMap
La clé de est un moyen pratique de le faire. Avec cela en place, nous pouvons ajouter le AbortController
signal au fetch
demande, abandonnez tous les contrôleurs précédents, ajoutez-en de nouveaux et supprimez-les à la fin.
J’espère que tout cela a du sens.
Maintenant, si nous frappons plusieurs fois le bouton d’envoi de ce formulaire, nous pouvons voir que toutes les demandes d’API, à l’exception de la plus récente, sont annulées.

Cela signifie que toute fonction répondant à cette réponse HTTP se comportera davantage comme prévu. Maintenant, si nous utilisons la même logique de comptage et de journalisation que nous avons ci-dessus, nous pouvons écraser le bouton d’envoi sept fois et verrions six exceptions (en raison de la AbortController
) et un journal de « 7 » dans la console. Si nous soumettons à nouveau et laissons suffisamment de temps pour que la demande soit résolue, nous verrons « 8 » dans la console. Et si nous écrasons le bouton Soumettre un tas de fois, encore une fois, nous continuerons à voir les exceptions et le nombre de requêtes finales dans le bon ordre.
Si vous souhaitez ajouter un peu plus de logique pour éviter de voir DOMExceptions dans la console lorsqu’une requête est abandonnée, vous pouvez ajouter un .catch()
bloquer après votre fetch
requête et vérifiez si le nom de l’erreur correspond à « 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 });
Fermeture
Tout cet article était axé sur les formulaires améliorés par JavaScript, mais c’est probablement une bonne idée d’inclure un AbortController
chaque fois que vous créez un fetch
demande. C’est vraiment dommage qu’il ne soit pas déjà intégré à l’API, mais j’espère que cela vous montre une bonne méthode pour l’inclure.
Il convient également de mentionner que cette approche n’empêche pas l’utilisateur de spammer le bouton d’envoi plusieurs fois. Le bouton est toujours cliquable et la demande se déclenche toujours, cela fournit simplement un moyen plus cohérent de traiter les réponses.
Malheureusement, si un utilisateur fait spam un bouton d’envoi, ces demandes iraient toujours vers votre backend et pourraient utiliser un tas de ressources inutiles.
Certaines solutions naïves peuvent désactiver le bouton d’envoi, utiliser un anti-rebond ou créer de nouvelles demandes uniquement après la résolution des précédentes. Je n’aime pas ces options car elles reposent sur le ralentissement de l’expérience de l’utilisateur et ne fonctionnent que du côté client. Ils ne traitent pas les abus via des requêtes scriptées.
Pour traiter les abus d’un trop grand nombre de requêtes adressées à votre serveur, vous souhaiterez probablement mettre en place une limitation de débit. Cela dépasse le cadre de cet article, mais cela valait la peine d’être mentionné. Il convient également de mentionner que la limitation du débit ne résout pas le problème initial des demandes en double, des conditions de concurrence et des mises à jour incohérentes de l’interface utilisateur. Idéalement, nous devrions utiliser les deux pour couvrir les deux extrémités.
Quoi qu’il en soit, c’est tout ce que j’ai pour aujourd’hui. Si vous voulez regarder une vidéo qui traite du même sujet, regardez ceci.
Merci beaucoup d’avoir lu. Si vous avez aimé cet article, merci de le partager.