Se você já usou a API fetch
de JavaScript para aprimorar o envio de um formulário, há uma boa chance de ter introduzido acidentalmente um bug de solicitação duplicada/condição de corrida. Hoje, vou orientá-lo sobre o problema e minhas recomendações para evitá-lo.
(Vídeo no final se preferir)
Vamos considerar um muito básico
<form method="post"> <label for="name">Name</label> <input id="name" name="name" /> <button>Submit</button> </form>
Quando pressionamos o botão enviar, o navegador atualiza toda a página.
Observe como o navegador é recarregado depois que o botão enviar é clicado.
A atualização da página nem sempre é a experiência que queremos oferecer aos nossos usuários, então uma alternativa comum é usarfetch
.
Uma abordagem simplista pode se parecer com o exemplo abaixo.
Depois que a página (ou componente) é montada, pegamos o nó DOM do formulário, adicionamos um ouvinte de evento que constrói uma solicitação fetch
usando o formuláriopreventDefault()
do 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(); }
Agora, antes que qualquer figurão do JavaScript comece a twittar para mim sobre GET vs. POST e solicitar corpo efetch
deliberadamente simples porque esse não é o foco principal.
A questão chave aqui é o event.preventDefault()
. Esse método impede que o navegador execute o comportamento padrão de carregar a nova página e enviar o formulário.
Agora, se olharmos para a tela e clicarmos em enviar, podemos ver que a página não é recarregada, mas vemos a solicitação HTTP em nossa guia de rede.
Observe que o navegador não recarrega a página inteira.
Infelizmente, ao usar JavaScript para evitar o comportamento padrão, introduzimos um bug que não existe no comportamento padrão do navegador.
Quando usamos simples
Se compararmos com o exemplo do JavaScript, veremos que todas as requisições são enviadas, e todas são completadas sem que nenhuma seja cancelada.
Isso pode ser um problema porque, embora cada solicitação possa levar um tempo diferente, elas podem ser resolvidas em uma ordem diferente da que foram iniciadas. Isso significa que, se adicionarmos funcionalidade à resolução dessas solicitações, poderemos ter algum comportamento inesperado.
Como exemplo, poderíamos criar uma variável para incrementar a cada requisição (“ totalRequestCount
“). Sempre que executamos a função handleSubmit
, podemos incrementar a contagem total, bem como capturar o número atual para rastrear a solicitação atual (“ thisRequestNumber
“).
Quando uma solicitação fetch
é resolvida, podemos registrar seu número correspondente no 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(); }
Agora, se apertarmos o botão enviar várias vezes, podemos ver números diferentes impressos no console fora de ordem: 2, 3, 1, 4, 5. Depende da velocidade da rede, mas acho que todos podemos concordar que isso não é o ideal.
Considere um cenário em que um usuário aciona várias solicitações fetch
em sucessão próxima e, após a conclusão, seu aplicativo atualiza a página com as alterações. O usuário pode, em última análise, ver informações imprecisas devido a solicitações resolvidas fora de ordem.
Isso não é um problema no mundo não-JavaScript porque o navegador cancela qualquer solicitação anterior e carrega a página após a conclusão da solicitação mais recente, carregando a versão mais atualizada. Mas as atualizações de página não são tão atraentes.
A boa notícia para os amantes de JavaScript é que podemos ter um
Só precisamos fazer um pouco mais de trabalho braçal.
Se você examinar a documentação da API fetch
, verá que é possível abortar uma busca usando um AbortController
e a propriedade signal
das opções fetch
. Parece algo assim:
const controller = new AbortController(); fetch(url, { signal: controller.signal });
Ao fornecer o sinal do AbortContoller
para a solicitação fetch
, podemos cancelar a solicitação sempre que o método abort
do AbortContoller
for acionado.
Você pode ver um exemplo mais claro no console JavaScript. Tente criar um AbortController
, iniciando a solicitação fetch
e, em seguida, executando imediatamente o método abort
.
const controller = new AbortController(); fetch('', { signal: controller.signal }); controller.abort()
Você deve ver imediatamente uma exceção impressa no console. Nos navegadores Chromium, deve aparecer “Uncaught (in promise) DOMException: The user aborted a request.” E se você explorar a guia Rede, deverá ver uma solicitação com falha com o texto de status “(cancelado)”.
Com isso em mente, podemos adicionar um AbortController
ao manipulador de envio do nosso formulário. A lógica será a seguinte:
AbortController
para quaisquer solicitações anteriores. Se existir, aborte-o.
AbortController
para a solicitação atual que pode ser anulada em solicitações subsequentes.
AbortController
correspondente.
Existem várias maneiras de fazer isso, mas usarei um WeakMap
para armazenar relacionamentos entre cada nó DOM <form>
enviado e seu respectivo AbortController
. Quando um formulário é enviado, podemos verificar e atualizar o WeakMap
de acordo.
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); }
O principal é ser capaz de associar um controlador de aborto ao seu formulário correspondente. Usar o nó DOM do formulário como a chave do WeakMap
é uma maneira conveniente de fazer isso.
Com isso definido, podemos adicionar o sinal do AbortController
à solicitação fetch
, abortar quaisquer controladores anteriores, adicionar novos e excluí-los após a conclusão.
Esperançosamente, tudo isso faz sentido.
Agora, se esmagarmos o botão de envio desse formulário várias vezes, podemos ver que todas as solicitações de API, exceto a mais recente, são canceladas.
Isso significa que qualquer função que responda a essa resposta HTTP se comportará mais conforme o esperado.
Agora, se usarmos a mesma lógica de contagem e registro que temos acima, podemos esmagar o botão enviar sete vezes e veremos seis exceções (devido ao AbortController
) e um log de “7” no console.
Se enviarmos novamente e dermos tempo suficiente para a solicitação ser resolvida, veremos “8” no console. E se pressionarmos o botão enviar várias vezes, novamente, continuaremos a ver as exceções e a contagem final de solicitações na ordem correta.
Se você deseja adicionar um pouco mais de lógica para evitar ver DOMExceptions no console quando uma solicitação é abortada, você pode adicionar um bloco .catch()
após sua solicitação fetch
e verificar se o nome do erro corresponde a “ 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 });
Todo este post foi focado em formulários aprimorados em JavaScript, mas provavelmente é uma boa ideia incluir um AbortController
sempre que você criar uma solicitação fetch
. É realmente uma pena que ainda não esteja embutido na API. Mas espero que isso mostre um bom método para incluí-lo.
Também vale a pena mencionar que essa abordagem não impede que o usuário envie spam várias vezes para o botão enviar. O botão ainda pode ser clicado e a solicitação ainda é disparada, apenas fornece uma maneira mais consistente de lidar com as respostas.
Infelizmente, se um usuário fizer spam em um botão de envio, essas solicitações ainda vão para o seu back-end e podem consumir um monte de recursos desnecessários.
Algumas soluções ingênuas podem desabilitar o botão enviar, usando um
Eles não abordam o abuso por meio de solicitações com script.
Para resolver o abuso de muitas solicitações ao seu servidor, você provavelmente deseja configurar alguns
Também vale a pena mencionar que a limitação de taxa não resolve o problema original de solicitações duplicadas, condições de corrida e atualizações de interface do usuário inconsistentes. O ideal é usar os dois para cobrir as duas pontas.
Enfim, isso é tudo que tenho para hoje. Se você quiser assistir a um vídeo que aborda esse mesmo assunto, assista a este.
Muito obrigado pela leitura. Se você gostou deste artigo, por favor
Originalmente publicado em