Skip to main content

Handle Promise Race Conditions

DON'T ๐Ÿ’ฉ๐Ÿงจ๐Ÿ’ฃ
function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);

useEffect(() => {
// ๐Ÿ’ฉ Avoid: Fetching without cleanup logic
fetchResults(query).then(json => {
setResults(json);
});
}, [query]);

return (
<>
<input placeholder='Search..' value={query} onChange={e => setQuery(e.target.value)} />
<SearchResults results={results} />
</>
);
}

Here on every keypress, we're triggering an API call.

Suppose we're searching for "Hello",
The response for the search query "He" might be significantly larger than the response for query "Hello", Hence the response for the search query "Hello" might arrive before the response for the query "Hell" or "He".

And which ever API call resolves last, will override all the previous responses.

So even though we have typed "Hello" to search,
We might see the results of "Hell" or "He" ๐Ÿ’ฉ๐Ÿ’ฉ

That's just one scenario of promise race conditions. There can be many reasons why the promises might not resolve in the order at which they were created.

  1. Different requests can take different amount to time to complete
  2. Network speed fluctuations can delay some API requests.

Asynchronous processes should never be expected to complete in order.

DO - Example 1 - Ignore the past API response โœ…
function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);

useEffect(() => {
let ignore = false;
fetchResults(query).then(json => {
if (!ignore) {
setResults(json);
}
});

// cleanup function
return () => {
ignore = true; // mark previous API to be ignored when "query" changes
};
}, [query]);

return (
<>
<input placeholder='Search..' value={query} onChange={e => setQuery(e.target.value)} />
<SearchResults results={results} />
</>
);
}
DO - Example 2 - Using AbortController โœ…
function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);

useEffect(() => {
const abortController = new AbortController();
fetchResults(
query,
abortController.signal,
).then(json => {
setResults(json);
});

// cleanup function
return () => {
abortController.abort(); // cancel previous API call when "query" changes
};
}, [query]);

return (
<>
<input placeholder='Search..' value={query} onChange={e => setQuery(e.target.value)} />
<SearchResults results={results} />
</>
);
}