Integrating Relay and Next.js

Ryan Delaney

Tuesday March 30th 2021

RevereCRE/relay-nextjs
Relay Hooks integration for Next.js apps
Go to the repo

At Revere we're creating a modern network across the commercial real estate industry — a large market network with similar data requirements to LinkedIn, Facebook, and other social platforms. Our graph of data includes employees, companies, commercial real estate deals, and much more. GraphQL has been great for building out our API and lets us iterate quickly on the frontend by removing need for tightly-coupled per-page APIs. We're using the incredible Next.js framework for our web app. Configuring the frontend build system, router, and lazy loading all take time away from product development, and our engineering team has been very happy with the quality of framework from the Next.js team. On top of these technologies we've been using Relay for data fetching throughout the app. It does a ton of heavy lifting and fixes many problems common when building a social network.

Revere Engineering has released a relay-nextjs, a library for integrating Next.js with Relay Hooks. We've been using it on our platform for a while now — first with the experimental releases of Relay Hooks and now using the production-ready version. It's a lightweight library that wires Relay into the proper places for server-side rendering, client-side navigation using React Suspense, and lazy loading data.

When we first began developing our app we would write data fetching code using the getServerSideProps API from Next.js. This worked pretty well for quite a while but we began to notice a few issues:

  1. Data is not cached in the client between renders. For example navigating to a user's employer and then back to their profile would make two queries for that user.
  2. Navigation required a round trip from the server before rendering the next page. Next.js does not allow you to add a loading indicator here making the app feel non-responsive.
  3. Local state updates became difficult because data was fetched and stored at the page-level component. For example if a user updates their status from a deeply nested component we would need to pipe that status update to the top-most component in the tree, overwrite the state, then have it propagate back down. Not too bad in the simple case but it complicates the code with each additional type of update.

Our engineering team began to look at GraphQL clients that we could use that eases these pain points. Apollo Client and urql both fixed #1 and #2, but Relay stands out as the client that fixed #3. Composing GraphQL fragments rather than making one large query allows each component to specify what data it needs and automatically update when the UI issues a mutation.

We started looking into Relay before the new Hooks API was officially released. Although the old API was workable the new hooks-based one was truly incredible. We started building on the new "unstable" foundation and found a few issues when trying to integrate it into our Next.js app. Relay was designed at Facebook and is used with their own in-house router and larger app framework so it makes sense that the core has no integration open-source solutions in a similar space, but to use it effectively we had to make sure it integrated well with Next.js. There are a few requirements our app needs that Next.js supports, and a few more to ensure that we can progressivly adopt Relay:

  1. The Revere platforms needs server-side rendering for SEO. Despite the improvements to web scrapers in the past few years we've found that plain-old server-rendered HTML generally works the best.
  2. The app should integrate with our authentication platform (Firebase Auth with a sprinkle of magic for SSR).
  3. Proper handling of 404 errors. Pages that don't exist should return a 404 code rather than a 200 OK or 500 error. This is often an issue when it comes to client-side rendered apps, but not Next.js apps using getServerSideProps. We'd like to keep this behavior, otherwise it would be a regression.
  4. Pages that don't use Relay should not have to be updated. We don't want to have to rewrite our app and we want to test out Relay on a few pilot pages before using it everywhere.

After some deep research into both frameworks we were able to satisfy each of these requirements. We abstracted this into a common library and open-sourced it as relay-nextjs. The main goal for this library is to be the glue layer and not replace or paper over either framework. We didn't want our developers looking at some (probably outdated) internal documentation to learn how to use Relay or Next.js, instead just use the open-source documentation. As such this library is limited in scope but we've found it flexible enough to meet the requirements of our platform.

Let's see what an example page using relay-nextjs looks like:

// src/pages/user/[uuid].tsx
import { withRelay, RelayProps } from 'relay-nextjs';
import { graphql, usePreloadedQuery } from 'react-relay/hooks';
import { getClientEnvironment } from './relay_client_environment';

// The $uuid variable is injected automatically from the route.
const ProfileQuery = graphql`
  query profile_ProfileQuery($uuid: ID!) {
    user(id: $uuid) {
      id
      firstName
      lastName
    }
  }
`;

// relay-nextjs automatically makes sure this query is loaded.
function UserProfile({ preloadedQuery }: RelayProps<{}, profile_ProfileQuery>) {
  const query = usePreloadedQuery(ProfileQuery, preloadedQuery);

  return (
    <div>
      Hello {query.user.firstName} {query.user.lastName}
    </div>
  );
}

function Loading() {
  return <div>Loading...</div>;
}

export default withRelay(UserProfile, UserProfileQuery, {
  fallback: <Loading />,
  createClientEnvironment: () => getClientEnvironment()!,
  serverSideProps: async (ctx) => {
    const { getAuthToken } = await import('lib/server/auth');
    const token = await getAuthToken(ctx);
    if (token == null) {
      return {
        redirect: { destination: '/login', permanent: false },
      };
    }

    return { token };
  },
  createServerEnvironment: async (ctx, { token }: { token: string }) => {
    const { createServerEnvironment } = await import('lib/server_environment');
    return createServerEnvironment(token);
  },
});

We can see that relay-nextjs takes care of telling Relay to fetch our query, managing when to show a loading state, and also gives us the necessary configuration to allow implementing something like authentication. We have more detailed examples and guides on how to set it up in your app in our documentation.

We love Relay and GraphQL at Revere and are happy to share our work with the community! All of this code can be found on GitHub and we hope to release more libraries like this in the future. If this is all interesting to you and Revere sounds like somewhere you'd want to work please reach out to our development team — we'd love to hear from you!