visit
If you’ve ever used JavaScript fetch
API to enhance a form submission, there’s a good chance you’ve accidentally introduced a duplicate-request/race-condition bug. Today, I’ll walk you through the issue and my recommendations to avoid it.
Let’s consider a very basic
<form method="post">
<label for="name">Name</label>
<input id="name" name="name" />
<button>Submit</button>
</form>
Notice how the browser reloads after the submit button is clicked.
The page refresh isn’t always the experience we want to offer our users, so a common alternative is to use fetch
API.
After the page (or component) mounts, we grab the form DOM node, add an event listener that constructs a fetch
request using the form preventDefault()
method.
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();
}
Now, before any JavaScript hotshots start tweeting at me about GET vs. POST and request body and fetch
request deliberately simple because that’s not the main focus.
The key issue here is the event.preventDefault()
. This method prevents the browser from performing the default behavior of loading the new page and submitting the form.
Notice the browser does not do a full page reload.
When we use plain
If we compare that to the JavaScript example, we will see that all of the requests are sent, and all of them are complete without any being canceled.
This may be an issue because although each request may take a different amount of time, they could resolve in a different order than they were initiated. This means if we add functionality to the resolution of those requests, we might have some unexpected behavior.
As an example, we could create a variable to increment for each request (“totalRequestCount
“). Every time we run the handleSubmit
function, we can increment the total count as well as capture the current number to track the current request (“thisRequestNumber
“).
When a fetch
request resolves, we can log its corresponding number to the 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();
}
Consider a scenario where a user triggers several fetch
requests in close succession, and upon completion, your application updates the page with their changes. The user could ultimately see inaccurate information due to requests resolving out of order.
The good news for JavaScript lovers is that we can have both a
If you look at the fetch
API documentation, you’ll see that it’s possible to abort a fetch using an AbortController
and the signal
property of the fetch
options. It looks something like this:
const controller = new AbortController();
fetch(url, { signal: controller.signal });
By providing the AbortContoller
‘s signal to the fetch
request, we can cancel the request any time the AbortContoller
‘s abort
method is triggered.
You can see a clearer example in the JavaScript console. Try creating an AbortController
, initiating the fetch
request, then immediately executing the abort
method.
const controller = new AbortController();
fetch('', { signal: controller.signal });
controller.abort()
With that in mind, we can add an AbortController
to our form’s submit handler. The logic will be as follows:
AbortController
for any previous requests. If one exists, abort it.
AbortController
for the current request that can be aborted on subsequent requests.
AbortController
.
There are several ways to do this, but I’ll use a WeakMap
to store relationships between each submitted <form>
DOM node and its respective AbortController
. When a form is submitted, we can check and update the WeakMap
accordingly.
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);
}
The key thing is being able to associate an abort controller with its corresponding form. Using the form’s DOM node as the WeakMap
‘s key is a convenient way to do that.
With that in place, we can add the AbortController
‘s signal to the fetch
request, abort any previous controllers, add new ones, and delete them upon completion.
This means any function responding to that HTTP response will behave more as you would expect.
Now, if we use that same counting and logging logic we have above, we can smash the submit button seven times and would see six exceptions (due to the AbortController
) and one log of “7” in the console.
If you want to add some more logic to avoid seeing DOMExceptions in the console when a request is aborted, you can add a .catch()
block after your fetch
request and check if the error’s name matches “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
});
This whole post was focused on JavaScript-enhanced forms, but it’s probably a good idea to include an AbortController
any time you create a fetch
request. It’s really too bad it’s not built into the API already. But hopefully, this shows you a good method for including it.
Unfortunately, if a user does spam a submit button, those requests would still go to your backend and could use consume a bunch of unnecessary resources.
Some naive solutions may be disabling the submit button, using a
To address abuse from too many requests to your server, you would probably want to set up some
Thank you so much for reading. If you liked this article, please
Also published .