If you choose to save all form inputs with useState
Hooks while using async validation, you will face this issue. Here is how to avoid multiple api calls and weird validation behaviors.
1. Setup
Here is the repository containing the solution, but I described the required setup steps in this article as well.
I re-created the issue we faced at work by generating both frontend and backend from template generators. For the frontend I used Create React App with typescript and for the backend I used ASP.NET Core webapi template.
Backend is not really important here, but we use this stack so it was easier to reason about how everything works.
For a quick setup, just create an empty folder and run the following commands
1 | npx create-react-app test-react-validation --template typescript |
Frontend setup
Since Availity reactstrap Validation doesn’t have types npm package, I had to modify react-app-env.d.ts
with
1 | declare module "availity-reactstrap-validation"; |
I created new file for the component as src/MyForm/MyForm.tsx
with the following code
1 | import React, { FC, useState } from "react"; |
And I changed App.tsx
to contain only the MyForm
component
1 | import React from 'react'; |
Of course, the code will not compile at this point because the implementation of the validate
method is missing.
To start the frontend project, in the project root use the command
1 | npm start |
Backend setup
Backend app required some modifications to enable CORS based on the following Stack Overflow answer.
In Startup.cs
you need to add the following to ConfigurationServices
method
1 | services.AddCors(options => |
and the following to Configure
method
1 | app.UseCors(); |
And finally I changed WeatherForecastController.cs
on line 34
to disable randomization, from
1 | Summary = Summaries[rng.Next(Summaries.Length)] |
to
1 | Summary = Summaries[index] |
To start the backend project, in the project root use the command
1 | dotnet run |
2. The issue
When I applied the solution suggested by Availity docs under Custom / Async
1 | let validateTimeout: number = 0; |
I got the following behavior
As you can see, there are several issues here:
- Validation was triggered on form load
- On each key press validation was triggered more then once
- Validation was triggered by entering value in a different field
- Validation was triggered by clicking the Submit button even if the value wasn’t changed
I was typing fast in order to take advantage debounce
. In this example, the api was called 8 times.
What is really happening here is that by calling onChange method, re-render of the whole component is triggered which also triggers the validation again.
3. The fix
A colleague suggested to use the useRef
Hook and I used it to decorate the validate
method. It stores the mutable value and doesn’t cause a re-render.
I also removed setTimeout
because it was not needed.
1 | const validate = useRef( |
Good progress! We are down to 4 api calls and onChange
calls no longer trigger validate
method!
What we want now is to track validation value so we don’t call the api more then once for the same value. We also don’t want to validate on the component load.
I used useRef
Hook again to keep track of the value returned from the api and whether or not that resulted in a successfull validation.
1 | const validatedWeatherSummary = useRef({ value: undefined as string | undefined, valid: false }).current; |
The result can be seen here
Finally, only one api call! Since React Hooks are still pretty new, I hope that someone will find this usefull.
To take screenshots I use LightShot
To record gifs I use ScreenToGif