Asynchronous calls in React : useEffect() + useState()

Avoid the mistake that many do : using useMemo() for retrive data from some long running APIs

How we do properly handle asyncronous call such retrieve data from some APIs?

Well let's keep it simple and assume that those data once retrieve are going to be used in few places so a useState() or few of them will be enough, so in following example is not used any Redux or other global state management.

So we would like to re-render our component once the asynchronous call is over.

Many would think about memoisation and so use useMemo() but unfortunately this would not be the best approach, actually even a bad one as even in React specifically mentions that useMemo should not be used to manage side effects like asynchronous API calls.

This is because triggering an async call is a side effect, so it should not be performed during the render phase - neither inside the main body of the component function, nor inside useMemo(...) which also happens during the render phase. Instead all side effects should be triggered inside useEffect.

We should instead you should use React's state that keeps the value of async calls returned and allow to trigger a re-render.

But there are some little adjustment that will even improve such approach, let's write some code and I will explain :

const [result, setResult] = useState()

useEffect(() => {
  let active = true
  load()
  return () => { active = false }

  async function load() {
    setResult(undefined) // this is optional
    const res = await someLongRunningApi(arg1, arg2)
    if (!active) { return }
    setResult(res)
  }
}, [arg1, arg2])

Here we call the async function inside useEffect.

Note that you cannot make the whole callback inside useEffect async( otherwise it will create a loop) - that's why instead we declare an async function load inside and call it without awaiting.

The effect will re-run once one of the args changes - this is what is needed in most cases.

So we can make sure to memoise args if in need to re-calculate them on render.

Doing setResult(undefined) is optional - and let in this way have fresh data at each render but one could simply need to keep the previous result on the screen until the next result is availabe. Or one might do something like setLoading(true) so the user knows what's going on.

Using active flag is important. Without it we are exposing ourself to a race condition waiting to happen: the second async function call may finish before the first one finishes:

Those are what then will happens in a race condition:

start first call

start second call

second call finishes, setResult() happens

first call finishes, setResult() happens again, overwriting the correct result with a stale one

Our component ends up in an inconsistent state. We avoid that by using useEffect's cleanup function to reset the active flag:

set active#1 = true, start first call

arg changes, cleanup function is called, set active#1 = false

set active#2 = true, start second call

second call finishes, setResult() happens

first call finishes, setResult() doesn't happen since active#1 is false

This is one of the use case in which useEffect + useState( or in case of large application, a global state management) this useEffect is the best approach and actually the one to go for deal with side effects such APIs calls.