Why we probably shouldn't put data fetching inside useEffect

#react

if you're used to React Hooks, you might have used useEffect this way many times where you fire off a promise inside it and keep track of the data and the error, and the pending state with local component states.

It might look something like this:

function Foo({ query }) {
	const [status, setStatus] = useState('idle')
	const [data, setData] = useState(null)
	const [error, setError] = useState(null)


	useEffect(() => {
	    setStatus('pending')
	    api(query).then(
	        data => {
	            setStatus('resolved')
	            setData(data)
	        },
	        error => {
	            setError(error)
	            setStatus('rejected')
	        }
	    )
	}, [query])
	

	if(status === 'idle') {
	    return //...
	}

	
}

Ryan Florence referred to this type of data fetching as “component fetching”. It is simple and it works. There is also merit to this approach - by coupling data fetching to a component we have the dependencies needed co-located with the component and we don't need to worry about having them fall out of sync with the UI.

However, this approach is not ideal for a number of reasons:

  1. We store fetched data inside the local state of the component (or we store it inside the state of the hook if we extract useEffect along with the fetching logic into a custom hook). Once the component unmount and mount again, we would have to make a second request and users have to wait for the pending state.
  2. If multiple components on a page that are requesting the same data. Now we have problems of scaling.
  3. If we manually cache the fetched data inside some global store, like redux store, then we need to manually invalidate the cache. Many times we forget to do that. If we are requesting the same data from two different components, then we could have one component with up-to-date fetched data and the other with stale data.
  4. We might need to implement retry to account for the case where the request is not successful.
  5. Most importantly, fetching in useEffect means the fetching only starts after the component renders. This leads to slow network waterfalls, because the children components will not start to fetch their data until their parents finish rendering.

The opposite of "component fetching" is “route fetching”, where the async fetching logic is sitting somewhere that can be statically accessed (i.e. Remix loaders). Cory House had a tweet on this.

Another alternative is React Query, with which you can prefetch in parent components. The advantage with using React Query is that all the other issues I mentioned above, such as duplicate requests, issues with stale cache and cache busting strategies, and even the ability to retry requests, are all taken care of.

Lastly, if you are just using plain React, the workaround here is to hoist the data fetching logic at the top level and fire them in parallel with Promise.all and then pass them down as props.

As Dan wrote in A Complete Guide to useEffect, in the future Suspense is going to take over the role of data fetching that useEffect has had.