0

The new Relay hooks API has put a focus on the React pattern of "render-as-you-fetch" and so far I am really liking this. Relay's useQueryLoader and usePreloadedQuery hooks make implementing this most of the time pretty straight forward.

I am however, struggling to find a good pattern on how to implement this pattern when it comes to routing. There are two typical situations that I find makes this difficult to implement.

Situation A:

  1. User loads a home page (example.com/)
  2. User go deep down one part of the app tree (example.com/settings/user/security/authentication)
  3. They then click on a link to take them to a totally unrelated part of their app (example.com/blog/post-1)

Situation B:

  1. User uses the URL bar to go to a section of the app instead of using a link (example.com/blog/post-1)

With these examples there are two outcomes, either the user goes to a route (example.com/blog/post-1) either via a nest child component or directly via the URL. So the way we are fetching data for this route must support both of these approaches.

I assume we would want to trigger the fetch as early as possible for this route, so when the user clicks on the link or as soon as we detect this route on page load.

There are three ideas I can think of to implement this:

  1. Use a fetch-then-render pattern instead (such as Relay's useLazyLoadQuery hook)
  2. Store a function (say in Context) and have all links for this route call this function in their onClick method, and also have a useEffect for this route that calls the function if there is no data loaded, or the reference for the query is stale
  3. Use render-as-you-fetch functions but implement them to support fetch-then-render also

Approach 1:

This defeats the purpose of render-as-you-fetch pattern however is an easy way out and more likely to be a "cleaner" way to implement fetching data for a route.

Approach 2:

In practice I have found this really hard to implement. Often the link to go to the route is disconnected from part of the component tree where the component renders the route is. And using a Context means that I have to manage different loadData functions for specific routes (which can be tricky when variables etc are involved).

Approach 3:

This is what I have been doing currently. In practice, it often results in being able to pass the load data function to a near by component, however if the route is accessed by a disconnected component, by the URL, or a page reload etc then the components falls back to calling the load data function in a useEffect hook.

Does anyone have any other ideas or examples on how they implemented this?

Charklewis
  • 4,427
  • 4
  • 31
  • 69

2 Answers2

4

An update on this topic, React Router v6 recently introduced support for route loaders, allowing preload Relay queries based on routing.

Example:

import { StrictMode, Suspense } from "react";
import ReactDOM from "react-dom/client";
import {
  createBrowserRouter,
  Link,
  RouterProvider,
  useLoaderData,
} from "react-router-dom";
import graphql from "babel-plugin-relay/macro";
import {
  loadQuery,
  PreloadedQuery,
  RelayEnvironmentProvider,
  usePreloadedQuery,
} from "react-relay";
import { environment } from "./environment";
import { srcGetCurrentUserQuery } from "./__generated__/srcGetCurrentUserQuery.graphql";

const getCurrentUser = graphql`
  query srcGetCurrentUserQuery {
    viewer {
      id
      fullname
    }
  }
`;



const Test = () => {
  const data = usePreloadedQuery(getCurrentUser, preloadedQuery);
  const preloadedQuery = useLoaderData() as PreloadedQuery<srcGetCurrentUserQuery>;

  return (
    <Suspense fallback={<>Loading...</>}>
      <Viewer preloadedQuery={preloadedQuery} />
    </Suspense>
  );
};

const router = createBrowserRouter([
  {
    element: (
      <>
        {"index"} <br /> <Link to={"/test"}>Go test</Link>
      </>
    ),
    path: "/",
  },
  {
    element: <Test />,
    path: "test",
    loader: async () => {
      return Promise.resolve(
        loadQuery<srcGetCurrentUserQuery>(environment, getCurrentUser, {})
      );
    },
  },
]);

ReactDOM.createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <RelayEnvironmentProvider environment={environment}>
      <RouterProvider router={router} />
    </RelayEnvironmentProvider>
  </StrictMode>
);

More information about React Router loaders here: https://reactrouter.com/en/main/route/loader

Charly Poly
  • 384
  • 1
  • 8
3

I've also been struggling with understanding this. I found these resources particularly helpful:

What I understand they aim for you to achieve is:

  • Start loading your query before and outside of the render path
  • Start loading your component at the same time as the query (code splitting)
  • Pass the preloaded query reference into the component

The way it's solved in the Relay demo is through something they call an "Entrypoint". These are heavily integrated into their router (you can see this in the Issue Tracker example). They comprise the following components:

  1. A route definition (e.g. /items)
  2. A lazy component definition (e.g. () => import('./Items'))
  3. A function that starts the query loading (e.g. () => preloadQuery(...))

When the router matches a new path, it starts the process of loading the lazy component, as well as the query. Then it passes both of these into a context object to get rendered by their RouterRenderer.

As for how to implement this, it seems like the most important rules are:

  1. Don't request data inside components, request it at the routing or event level
  2. Make sure data and lazy components are requested at the same time

A simple solution appears to be to create a component that is responsible for collecting the data, and then rendering the respective component. Something like:

const LazyItemDetails = React.lazy(() => import('./ItemDetails'))

export function ItemEntrypoint() {
   const match = useMatch()
   const relayEnvironment = useEnvironment()
   const queryRef = loadQuery<ItemDetailsQuery>(relayEnvironment, ItemDetailsQuery, { itemId: match.itemId })
   
   return <LazyItemDetails queryRef={queryRef} />
}

However there are potential issues that the Issue Tracker example adds solutions to:

  • The lazy component may have previously been requested so should be cached
  • The data fetching sits on the render path

Instead the Issue Tracker solution uses a router which does the component caching, and the data fetching at the same time as the route is matched (by listening to history change events). You could use this router in your own code, if you're comfortable with maintaining your own router.

In terms of off the shelf solutions, there doesn't appear to be a router that implements the patterns required to do fetch-as-you-render.

TL;DR Use the Relay Issue Tracker example router.

Bonus: I've written a blog post about my process of understanding this pattern

Ben Taylor
  • 41
  • 4
  • Hey there! I *think* I have a solution to the pattern using React Router. I'm pretty sure it meets all the requirements you laid out for render-as-you-fetch. I'd be interested in your take on the solution, specifically whether it does overcome all the problems you describe above. – Ben Wainwright Sep 02 '21 at 07:09
  • 1
    https://gist.github.com/benwainwright/63d05f4158cfa163b70fd28b0c697b10 – Ben Wainwright Sep 02 '21 at 07:09
  • Note, it can probably be simplified – Ben Wainwright Sep 02 '21 at 07:09
  • It definitely starts the loading of both the component and the data at the same time and neither blocks the other. I'm a little hazy about whether it suffers from the component caching issue though. – Ben Wainwright Sep 02 '21 at 07:11