Home > Back-end >  How can I start an async API fetch as early as possible in a React functional component?
How can I start an async API fetch as early as possible in a React functional component?

Time:10-23

The standard way to make an API call in functional React is with useEffect:

function Pizzeria() {
  const [pizzas, setPizzas] = useState([])
  
  useEffect(
    () => fetchPizzas().then(setPizzas),
    []
  )

  return (
    <div>
      {pizzas.map((p, i) => <Pizza pizza={p} key={i} />)}
    </div>
  )
}

But, as this article points out, useEffect will not fire until after the component has rendered (the first time). Obviously in this trivial case it makes no difference, but in general, it would be better to kick off my async network call as soon as possible.

In a class component, I could theoretically use componentWillMount for this. In functional React, it seems like a useRef-based solution could work. (Allegedly, tanstack's useQuery hook, and probably other libraries, also do this.)

But componentWillMount is deprecated. Is there a reason why I should not do this? If not, what is the best way in functional React to achieve the effect of starting an async call early as possible (which subsequently sets state on the mounted component)? What are the pitfalls?

CodePudding user response:

You're splitting milliseconds here, componentWillMount/render/useEffect all happen at essentially the same time, and the time spent fetching occurs after that. The difference in time from before to after rendering is tiny compared to the time waiting for the network when the request is sent. If you can do the fetch before the component renders, react-query's usePrefetch is nice for that.

CodePudding user response:

Considering the scope of a single component, the earliest possible would be to just make the call in the component's function. The issue here is just that such statement would be executed during every render.

To avoid those new executions, you must keep some kind of "state" (or variable, if you will). You'll need that to mark that the call has been made and shouldn't be made again.

To keep such "state" you can use a useState or, yes, a useRef:

function Pizzeria() {
  const pizzasFetchedRef = useRef(false)
  const [pizzas, setPizzas] = useState([])
  
  if (!pizzasFetchedRef.current) {
    fetchPizzas().then(setPizzas);
    pizzasFetchedRef.current = true;
  }

Refs are preferred over state for this since you are not rendering the value of pizzasFetched.

The long story...

Yet, even if you use a ref (or state) as above, you'll probably want to use an effect anyway, just to avoid leaks during the unmounting of the component. Something like this:

function Pizzeria() {
  const pizzasFetchStatusRef = useRef('pending'); // pending | requested | unmounted
  const [pizzas, setPizzas] = useState([])
  
  if (pizzasFetchStatusRef.current === 'pending') {
    pizzasFetchStatusRef.current = 'requested';
    fetchPizzas().then((data) => {
      if (pizzasFetchStatusRef.current !== 'unmounted') {
        setPizzas(data);
      }
    });
  }
  useEffect(() => {
    return () => {
      pizzasFetchStatusRef.current = 'unmounted';
    };
  }, []);

That's a lot of obvious boilerplate. If you do use such pattern, then creating a custom hook with it is the better way. But, yeah, this is natural in the current state of React hooks. See the new docs on fetching data for more info.

One final note: we don't see this issue you pose around much because that's nearly a micro-optimization. In reality, in scenarios where this kind of squeezing is needed, other techniques are used, such as SSR. And in SSR the initial list of pizzas will be sent as prop to the component anyway (and then an effect -- or other query library -- will be used to hydrate post-mount), so there will be no such hurry for that first call.

  • Related