I'm new to React and I want to implement a form where the user is given N inputs and is asked to copy/paste some contents in it (he can also click on a + or - button to add or remove some inputs but let's keep it simple for now).
The idea is that, for each input, the user will copy/paste some data and, on each input an onChange listener is attached so that each time an input is changed, it will trigger a call to an API or backend service to validate the data. Once the data is validated, it will return whether or not the user was right (by changing the background color of the input for example). Here is a picture:
The problem is that, let say the user copies/pastes gorilla in the first input and the async call takes 10s, but then 2s after it copied/pasted gorilla in the first input, he copies/pastes spaceship in the second input (and this async call takes 4s to return let's say). What happens with my current version is that, since the 2nd call finished before the first one, isValid is updated for the second input while it is not for the first input.
This is due to the fact that I'm using useEffect and the clean function. I have also tried to use useState instead but each time I ran into a race condition...
Notes:
- If the user entered gorilla in the first input and 1s after he modifies the same input and enters giraffe. If the first async call did not finish yet, I prefer to kill it and only retrieve the validation for the latest call made for that same input (this is why I used a clean function in my
useEffect) - Even if the problem of stopping the validation of other inputs is solved, I think there will always be a race condition as in my
useEffectI am modifying my useState associated to each of my input so I'm not sure how to go about it.
Here is tiny sample to replicate my current behavior:
import React, { useEffect, useState } from "react";
export default function MyComponent(props) {
let delay = 10000; // ms
const [data, setData] = useState(
Array(5).fill({
isValid: 0,
url: null
})
);
const [inputIdx, setInputIdx] = useState(-1);
const updateData = (index, datum) => {
const updatedData = data;
updatedData[index] = datum;
setData([...updatedData]);
setInputIdx(index);
delay /= 2;
};
useEffect(() => {
let canceled = true;
new Promise((resolve) => {
setTimeout(() => resolve("done"), delay);
}).then((e) => {
if (canceled) {
const updatedData = data;
updatedData[inputIdx].isValid = true ^ updatedData[inputIdx].isValid;
setData([...updatedData]);
}
});
return () => {
canceled = false;
};
}, [inputIdx]);
const processData = data.map((datum, index) => {
return (
<input
type="text"
onChange={e =>
updateData(index, {
url: e.target.value === "" ? null : e.target.value,
isValid: data[index].isValid,
})
}
style={{display:"block"}}
/>
);
});
return (
<div>
<div>{processData}</div>
<div>{JSON.stringify({data})}</div>
</div>
);
}
Here is a CodeSandBox
If you enter "a" in the first input and just after "b" in the second input and wait for a few seconds, you'll see that isValid becomes 1 for the 2nd input and not for the 1st input (Here I toggle isValid so if it was 0 before it will be 1 after, and if it was 1 it will be 0 after the call).
Also, here, to imitate a call to the backend I used setTimeout with 10s for the first call and 5s for the second call and 5/2=2.5s for the third call, ....
Do you have any idea on what I can do to solve my problem? I guess I should change the architecture itself but as I'm new to React and did not find any similar problems, I'm asking you for help.
Thank you!


canceled = false, do you agree I will bump into a race condition as different API calls might callsetData()at the same time (or is it not possible?). Also I want to keep this behavior of throwing away any other currently processing API calls (but only if these API calls were triggered from the same input)