React Async Data fetch

Sherry Hsu
3 min readJun 14, 2021


A few React characteristics to keep in mind:

State Update is Asynchronous (slower than what you expect!)

  • State updates are asynchronous , so be careful with using the state immediately inside the same function. E.g. in the handleChange() below, the state input is not updated immediately after setInput is called. Therefore, if we fetch data using the input state, the value of input would actually be before the update!
const handleChange = (event) => {  setInput(;
fetchData(input); // x , prev input value
fetchData(; // o , current value
  • However, the useEffect hook is run after the state updates and DOM updates, our fetchData() would get the updated input value if placed inside a useEffect hook
console.log(input) // o , current input value
},[input])const handleChange = (event) => { setInput(;
fetchData(input); // x , prev input value
fetchData(; // o , current input value
  • If we are calling async/ awaitfetchData() inside useEffect , this would not work inuseEffect(async ()=>{},[]) . It is because useEffect expects a clean-up function or nothing to be returned but async functions return an implicit promise! We should define the async fetchData functions separately instead of using the useEffect wrapping function
  • Since useEffect requires us to explicitly declare the dependencies in the array, it is best to define the fetchData functions and call it inside the useEffect functions so that we don’t need to worry about declaring fetchData as a dependency in useEffect !
async function fetchData(input) {...}
const data = await fetchData()
},[input])const handleChange = (event) => {

Debounce and Re-render

Re-render = Reborn of var, func inside components

  • On each component re-render, the functions and the variables inside are created brand new and only the states are updated and preserved across the renders
  • Therefore, if we apply the debounced functions simply by itself inside a React component, debounced does not work as expected! Why?
  • Debounce works by terminating the intermediate function calls within the timeout period and therefore reduces the total number of functional invocations. e.g. if an user types ‘cat’ within a timeout of 300ms, a debounced onChange handler only invokes the function once with the input ‘cat’ because the previous invocations with input of ‘c’ and ‘ca’ are cancelled
Debounce image from CSS-Tricks
Debounce Image from CSS-Tricks
  • In React, this presents a dilemma because… 1. on state update, the React component re-renders 2. in each re-render, functions are created brand new and executed 3. It means that, the intermediate invocations of the debounced functions still get executed as the functions are brand new in each render, as they have no memory of the time lapsed from the previous render!
Red arrow shows executing debounce across re-rendering without useCallback
  • To solve this issue, we want the same function to be preserved and called throughout the re-rendering phase; therefore, useCallback comes to rescue!
  • useCallback returns a memoized version of the function that only changes when one of the dependencies changes. If the dependency does not change, the same function would be used across re-rendering and debounce as expected!
  • This gist contains more detailed example
function MySearch() {  const [input, setInput] = useState('')  // create memoized version of debounced data fetch function  const debouncedFetchData = useCallback(debounce(fetchData, 500),   []);  // call memoized debounced function in the event handler so the 
intermediate searches are cancelled by debounce while the user types
const handleChange = (event) => {
return <input onChange={handleChange} value={input} />}




Sherry Hsu

A software engineer passionate about learning and growth