React Async Data fetch
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 stateinput
is not updated immediately aftersetInput
is called. Therefore, if we fetch data using theinput
state, the value ofinput
would actually be before the update!
const handleChange = (event) => { setInput(event.target.value);
fetchData(input); // x , prev input value
fetchData(event.target.value); // o , current value};
- However, the useEffect hook is run after the state updates and DOM updates, our
fetchData()
would get the updatedinput
value if placed inside auseEffect
hook
useEffect(()=>{
console.log(input) // o , current input value },[input])const handleChange = (event) => { setInput(event.target.value);
fetchData(input); // x , prev input value
fetchData(event.target.value); // o , current input value};
- If we are calling async/ await
fetchData()
insideuseEffect
, this would not work inuseEffect(async ()=>{},[])
. It is becauseuseEffect
expects a clean-up function or nothing to be returned butasync
functions return an implicit promise! We should define theasync
fetchData functions separately instead of using theuseEffect
wrapping function - Since
useEffect
requires us to explicitly declare the dependencies in the array, it is best to define thefetchData
functions and call it inside the useEffect functions so that we don’t need to worry about declaringfetchData
as a dependency inuseEffect
!
useEffect(()=>{
async function fetchData(input) {...}
const data = await fetchData()
setData(data)},[input])const handleChange = (event) => {
setInput(event.target.value)}
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
- 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!
- 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) => {
setInput(event.target.value);
debouncedFetchData(event.target.value);
}; return <input onChange={handleChange} value={input} />}