Understanding render-as-you-fetch with React & Relay
Tags: react, relay, javascript, graphql
Published: 2021-06-03 06:51:55

I've been moving an existing codebase to a GraphQL API over the last few weeks using [Relay](https://relay.dev/) as the front-end client. One thing I've been struggling with has been implementing the render-as-you-fetch (or fetch-as-you-render) pattern. A big part of the difficulty here is how our tools rely on the render path for coordinating work. I'm using this article as a way to write down what I've learned researching and figuring out this pattern in practice.

# What is render-as-you-fetch?

I'm not sure about the origin of the idea, but there's a great explanation of it in the [ReactConf 2019 demo of Relay](https://www.youtube.com/watch?v=JDDxR1a15Yo&t=3647s). There's also some good explanations in the [React Docs for Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html#approach-3-render-as-you-fetch-using-suspense).

The basic idea is that the render path of your components is a bad place to load data. The simplest reason is that it can be blocked by other components loading. If you only load data on the render path, you can be susceptible to waterfalls of loads. The worst case is one component blocks a number of other components from rendering, then when it unblocks them all of those components need to load their own data.

Imagine a profile page for a user:

```jsx
function ProfilePage({ userId }) {
  const [isLoaded, profileData] = useProfileDataFetcher(userId)
  if (!isLoaded) {
    return <LoadingSpinner />
  }
  return (<>
    <ProfileHeader profile={profileData} />
    <PhotoCarousel photoIds={profileData.recentPhotoIds} />
    <PostList postIds={profileData.recentPostIds} />
  </>)
}
```

You could imagine that the `PhotoCarousel` component and the `PostList` component both need to go get their own data. So you have one fetch (the profile data) blocking two more fetches. Each of those components could also be fetching data, such as comments, avatars etc. This creates a cascade of loading symbols like:

![Waterfall Loading](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/evk6hgv8xdi7f4xan5lb.png)

When the first component finishes loading, it reveals its dependent child components - which of course now need to load!

These waterfalls show a real flaw in the pattern of loading data inside a component (on the render path). It creates an awkward UX and makes your page much slower to load (even if your individual components are quite performant).

## An aside on Suspense for  Data Loading

To fully grasp the render-as-you-fetch pattern you also need to understand how [Suspense for Data Loading works](https://reactjs.org/docs/concurrent-mode-suspense.html). It's a really nifty pattern that works kind of like an Error Boundary. You set it up by creating a `Suspense` component with a fallback loading component:

```jsx
<Suspense fallback={<LoadingSpinner />}>
  <ProfilePage />
</Suspense>
```

Then if a component starts rendering, but is not yet ready to render you `throw` a `Promise` that will resolve when it's ready. To use it in our example we could modify our `useFetchProfileData` hook to throw if the data isn't finished loading.

```js
const profileFetcher = new ProfileDataFetcher()

function useProfileDataFetcher(userId) {
  profileFetcher.loadFromNetworkOrCache(userId)
  if (profileFetcher.isLoading(userId)) {
    throw profileFetcher.getPromise(userId)
  }
  return profileFetcher.getData(userId)
}
```

The Promise that we throw then gets waited on by the `Suspense` component until it's complete. In its place the `LoadingSpinner` is rendered. Once it's complete the component will continue rendering.

A neat result of this, is that we don't need to handle managing loading state within our component. Instead we can assume we _always_ have the data we depend on. This simplifies our `ProfilePage` quite a bit:

```jsx
function ProfilePage({ userId }) {
  const profileData = useProfileDataFetcher(userId)
  return (<>
    <ProfileHeader profile={profileData} />
    <PhotoCarousel photoIds={profileData.recentPhotoIds} />
    <PostList postIds={profileData.recentPostIds} />
  </>)
}
```

But it doesn't stop our waterfall cascade of loading spinners.
 

## Back to our Waterfall

The simplest solution to this problem would be to fetch all of the nested data in the `ProfilePage` component at once. The `ProfilePage` would load the profile data, the photos, the posts, the usernames etc. But this breaks down in a number of situations:

1. Nested routes - you can't know what data you'll need at each level until you evaluate the routes

2. Concurrent mode - your data-loading could be inside a component that has paused rendering

3. Slow components - the performance of your data loading is dependent on how fast your components evaluate

4. Re-rendering - each time your component is rendered it needs to retry fetching the data, even if it's unnecessary (e.g. a theme change)

The solution to all of these problems is **render-as-you-fetch**. Instead of putting the fetching code inside of your component, you put it outside the component, and make sure it happens **before** the render even occurs. Imagine something like:

```jsx
function ProfileButton({ userId, name }) {
  const router = useRouter()
  const clickAction = function() {
    profileFetcher.load(userId)
    router.navigateToProfilePage(userId)
  }
  return (<button onClick={clickAction}>{ name }</button>)
}
```

When the button is clicked the `clickAction` first loads the profile data, and then triggers navigation. This way the loading happens not only before the `ProfilePage` starts loading, but it happens outside of the render path. So complicated render logic has no way of impacting when the data gets loaded.

In relay this is all achieved using two hooks:

```js
// From a container
const [queryRef, loadQuery] = useQueryLoader(/*...*/)

// Inside your component
const data = usePreloadedQuery(queryRef, /*...*/)
```

The first provides us with a `loadQuery` function that can be called to start the query loading, and a `queryRef` that will refer to that state. The second takes the `queryRef` and returns the data - or suspends if it hasn't loaded yet. There's also a less safe `loadQuery` function provided by Relay that doesn't automatically dispose of data.

Our `ProfileButton` example above, when using Relay would become something like:

```jsx
function ProfileButton({ userId, name }) {
  const router = useRouter()
  const [queryRef, loadQuery] = useQueryLoader(/*...*/)
  const clickAction = function() {
    loadQuery(/*...*/, {userId})
    router.navigateToProfilePage(queryRef)
  }
  return (<button onClick={clickAction}>{ name }</button>)
}
```

And our `Profile` component would look like:

```jsx
function ProfilePage({ queryRef }) {
  const profileData = usePreloadedQuery(queryRef, /*...*/)
  return (<>
    <ProfileHeader profile={profileData} />
    <PhotoCarousel photos={profileData.recentPhotos} />
    <PostList posts={profileData.recentPosts} />
  </>)
}
```

Here the `queryRef` is passed on to the `ProfilePage` so that it has a handle for the data loading. Then the `usePreloadedQuery` call will suspend if the data is still loading.


# Routing with render-as-you-fetch

The big difficulty with all of this is that it starts falling apart when you consider routing. If you trigger fetching just before a navigation (like in the above example) what happens if the user visits that route directly? It would fail to load, because the `queryRef` hasn't been created.

In the [ReactConf 2019 Relay demo video](https://www.youtube.com/watch?v=JDDxR1a15Yo&t=3647s) that I linked earlier they solve this with a thing called an "Entrypoint". This is a concept that wraps up two tasks together:

1. Preloading data with `preloadQuery`
2. Retrieving the `lazy` component for the route

In this case the idea is that each routing entrypoint contains a helper for loading its data, and it uses [webpack codesplitting](https://webpack.js.org/guides/code-splitting/) for lazy-loading each route's component hierarchy.

Using `react-router` attempting this approach, the entrypoint would look something like:

```jsx
const Profile = lazy(() => import('./Profile'))

export function ProfileEntrypoint() {
    const { profileId } = useParams();
    const [queryRef, loadQuery] = useQueryLoader(/*...*/, { profileId })
    loadQuery()
    return (<Profile queryRef={queryRef} />)
}
```

And our routes would look like:

```jsx
<Router>
    <Header />
    <Switch>
        <Route path="/profile/:profileId">
            <ProfileEntrypoint />
        </Route>
    </Switch>
</Router>
```

**But this isn't going to work!**

Unfortunately we've violated one of the rules we created going in: we've put the data fetching on the render path. Because our entrypoint is a component, and we call `loadQuery` when the component renders, the loading happens in the render path. 

Our fundamental problem here is that the routing paths are evaluated during render, and not when the history object triggers a change. From what I understand it doesn't seem like its possible to resolve this. That means `react-router` is out. So is any router that evaluates its routes through components!


## Finding a suitable router

So now we need to find a suitable router that can support this pattern of requesting data outside of the render path. The relay community has built an [extension to Found](https://github.com/relay-tools/found-relay) - but it hasn't been updated for render-as-you-fetch. The [Found](https://github.com/4Catalyzer/found) router itself is quite flexible and extensible and so you could potentially implement entrypoints on top, but I haven't seen an example of this. As for other routers, I haven't seen any that aren't taking the `react-router` approach.

It seems like this is a problem that the `relay` team have seen in advance. Their [Issue Tracker example](https://github.com/relayjs/relay-examples/tree/master/issue-tracker) rolls its own routing system based off the same primitives used by `react-router`.

There's also a couple of routers that people have built after encountering this problem: [React Suspense Router](https://github.com/dai-shi/react-suspense-router) and [Pre-Router](https://github.com/kyarik/pre-router). Both are not very mature, but are promising. Pre-router particularly is quite clearly inspired by the Issue Tracker example.

Since they are rather immature, I think right now the best idea is to use the Router in the Issue Tracker example and maintain it yourself. This isn't a great solution, but it seems to be the only way forward for now.

Using the routing system from that example, our routes from before would instead look something like:

```jsx
const routes = [
  {
    component: JSResource('Root', () => import('./Root')),
    routes: [
      /* ... */
      {
        path: '/profile/:id',
        component: JSResource('Profile', () =>
          import('./Profile'),
        ),
        prepare: params => {
          return {
            queryRef: loadQuery(/* ... */, {id: params.id}),
          }
        },
      },
    ],
  },
]
```

Here we see the entrypoint pattern quite clearly. Each route is made up of a path to match, a component to fetch, and a prepare function that loads the appropriate query. The `JSResource` helper here will cache the returned component to make sure it doesn't get lazily requested multiple times. While the `prepare` function is used to trigger any preparation work for the route - in our case that's the `loadQuery` function that Relay provides.

What's particularly useful about this approach is how loading works with nested routes. Each of the nested routes will be matched all at once, and their prepare calls and components will be successively run. Once all the preparation work is done the rendering can start, and even if rendering blocks at a higher level the data has already started loading for the lower levels. Waterfall solved!


# Wrapping up

So that resolves our problem! But it does mean a lot of extra work for me, replacing our existing routing system with one that supports this new paradigm.

I hope this has helped you understand the render-as-you-fetch pattern, and helped you see how it might be implemented in practice using relay. If you know of a better solution to the routing problem, I'd love to hear it in the comments. Understanding all of this has been a bit of a wild ride for me, and I'm still getting my head around each of the required components. What seems like a straightforward idea at first ends up being more than a little complex.

**Edit:** Max Wheeler recommended on twitter that I check out [Atlassian's React Resource Router](https://atlassian-labs.github.io/react-resource-router/). It looks like a great solution for render-as-you-fetch for regular fetch requests, however its API isn't ideal for relay. It might work with some nice wrappers around its `useResource` method. Worth checking out!

**Edit2:** @gajus has recommended using YARR (github.com/contra/yarr) which seems to be a great solution to this problem.