Why you should look at Relay and GraphQL again

If you've been following my work for a long time, you know that one of my favorite passions is comparing GraphQL, REST, tRPC, and other technologies that don't mention Relay and Fragments. In this post, I'll explain why I think Relay is a game changer, how we've made it 100x easier to use and implement, and why you should consider it.

What makes Relay so special?

Stop thinking about APIs from the server's point of view for a moment. Instead, think about consuming APIs from an interface perspective and how you can create applications that are scalable and easy to maintain. This is where Relay shines, this is where you can see a significant difference between Relay, REST, tRPC and other technologies.

If you haven't used Relay before, you may have never realized how powerful GraphQL combined with Relay can be. The next section will explain why.

At the same time, many people are afraid of Relay because it has a steep learning curve. There is a perception that Relay is difficult to set up and use, and this is true to some extent. No doctorate should be required to use it.

That's why we created a first-class Relay integration in WunderGraph that works with both NextJS and pure React (using Vite for example). We want to make Relay more accessible and easier to use. Core features such as Server Side Rendering (SSR), Static Site Generation (SSG), Persisted Operations, and Rendering as Received (also known as Suspense) are built in and work out of the box.

Before we dive into how we've made Relay easy to use, let's first look at what makes Relay so special.

Placing Data Requirements Using Fragments

A typical pattern for fetching data in applications like NextJS is to fetch data in the root component and pass it to child components. With a framework like tRPC, you define a procedure that fetches all the data needed for one page and passes it to the children. This way you are implicitly defining the data requirements for the component.

Let's say you have a page that displays a list of blogs, and each blog has a list of comments.

In the root component, you would select the blogs with comments and pass the data to the blog component, which in turn passes the comments to the comment component.

Let's illustrate this with some code:

// in pages/index.tsx
export default function Home({ posts }: { posts: Post[] }) {
  return (
    <div>
      {posts.map((post) => (
        <BlogPost key={post.id} post={post} />
      ))}
    </div>
  );
}

// in components/BlogPost.tsx
export default function BlogPost({ post }: { post: Post }) {
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <div>
        {post.comments.map((comment) => (
          <Comment key={comment.id} comment={comment} />
        ))}
      </div>
    </div>
  );
}

// in components/Comment.tsx
export default function Comment({ comment }: { comment: Comment }) {
  return (
    <div>
      <h2>{comment.title}</h2>
      <p>{comment.content}</p>
    </div>
  );
}

As an example, the component Comment has two data dependencies: title And content. Let's say we use this component in 10 different places in our application. If we want to add a new field to a component CommentFor example, authorwe will have to figure out all the places where we use the component Commentgo to the root component, find the procedure that retrieves the data, and add a new field to it.

You can see how this can quickly become a huge support burden. The problem that leads to this is that we retrieve data from the top down. The result is a tight coupling between the data extraction logic and the components.

With Relay and Fragments, we can place the data requirements with the component while decoupling the data retrieval logic from the component. Together with data masking (next section), this is a breakthrough because it allows us to create reusable components that are separate from the data retrieval logic.

It's worth noting that GraphQL itself does not solve this problem. Moreover, most GraphQL clients do not encourage this pattern, leading to the same problems we saw with REST APIs.

So-called “God queries”, which retrieve all the data for a page, are a common pattern among GraphQL clients. Without Fragments, it's really the same problem as with a REST API or tRPC, just with different syntax and added GraphQL overhead.

Let's see how we can achieve this using Relays and Fragments.

// in pages/index.tsx
export default function Home() {
  const data = useFragment(
    graphql`
      query Home_posts on Query {
        posts {
          ...BlogPost_post
        }
      }
    `,
    null
  );

  return (
    <div>
      {data.posts.map((post) => (
        <BlogPost key={post.id} post={post} />
      ))}
    </div>
  );
}

// in components/BlogPost.tsx
export default function BlogPost({ post }: { post: Post }) {
  const data = useFragment(
    graphql`
      fragment BlogPost_post on Post {
        title
        content
        comments {
          ...Comment_comment
        }
      }
    `,
    post
  );

  return (
    <div>
      <h1>{data.title}</h1>
      <p>{data.content}</p>
      <div>
        {data.comments.map((comment) => (
          <Comment key={comment.id} comment={comment} />
        ))}
      </div>
    </div>
  );
}

// in components/Comment.tsx
export default function Comment({ comment }: { comment: Comment }) {
  const data = useFragment(
    graphql`
      fragment Comment_comment on Comment {
        title
        content
      }
    `,
    comment
  );

  return (
    <div>
      <h2>{data.title}</h2>
      <p>{data.content}</p>
    </div>
  );
}

In this example the component Comment completely separate from the data retrieval logic. It defines its data requirements in a fragment that is placed with the component. We can use the component Comment in any number of places, it is completely separate from the data retrieval logic.

If we want to add a new field to a component Commentfor example, field authorwe can simply add it to the fragment, and the component Comment will automatically receive a new field.

Changing our perspective to the data retrieval logic, we see that the component Home doesn't care what fields the component needs Comment. This logic is completely separate from the component Home using fragments.

Having said that, there is one more thing that makes truly separated components possible: data masking.

Reusable components through data masking

Let's say we have two adjacent components that both use comment data. Both define their data requirements in a separate fragment. One component only needs a field titlewhile another component requires a field author And content.

If we were to directly pass comment data to both components, we might accidentally use the field title in a component that did not define it in its fragment. This way we would introduce a dependency between the two components.

To prevent this, Relay allows us to mask the data before passing it to the component. If a component has not defined a field in its fragment, it will not be able to access it, even though it is theoretically available in the data.

To my knowledge, no other API client has this feature, so I think you shouldn't give up on GraphQL without trying Relay. GraphQL and Relay are worth the money when you compare them to tRPC, for example. It's important to understand the benefits so you can make an informed decision about whether it's worth it.

Many people think that GraphQL and Relay are only useful for huge applications. I think this is misleading. Creating reusable components is a huge benefit for any application, no matter its size. Once you've figured out chunks and data masking, you really won't want to go back to the old way of doing things.

We'll look at how easy we've made getting started with Relay and Fragments in the next section.

GraphQL Compile-Time Validation and Security

Another benefit of using Relay is that the “Relay Compiler” (recently rewritten in Rust) compiles, validates, optimizes, and stores all GraphQL operations during the build phase. With the right setup, we can completely “remove” the GraphQL API from production. This is a huge security benefit because it is impossible to access the GraphQL API from the outside.

Additionally, we can test all GraphQL operations during the build phase. Expensive operations such as normalization and verification are performed during the build phase, reducing runtime overhead.

How does WunderGraph make using Relay easier?

You may not be convinced of the benefits of Relay yet, but I hope you're at least interested in giving it a try.

Let's take a look at how the WunderGraph integration makes it easy to get started with Relay.

Setting up Relay + NextJS/Vite with WunderGraph is easy

We ourselves tried to set up Relay with NextJS and Vite. It is not simple. In fact, it's quite difficult. We found npm packages that try to bridge the gap between Relay and NextJS, but they weren't very well supported, the documentation was outdated and, most importantly, we felt they were too niche, e.g. forcing the use of getInitalPropswhich is deprecated in NextJS.

So we took a step back and created a solution that works with Vanilla React and frontend frameworks like NextJS and Vite without being too niche. We've built the necessary tools to make Server Side Rendering (SSR), Static Site Generation (SSG), and Render as You Receive easy to use with any frontend framework.

Additionally, we made sure to choose some sensible defaults, such as forcing operations to be saved to default without any configuration, giving the user a default secure experience without having to think about it.

So what does a simple setup look like?

// in pages/_app.tsx
import { WunderGraphRelayProvider } from '@/lib/wundergraph';
import '@/styles/globals.css';
import type { AppProps } from 'next/app';

export default function App({ Component, pageProps }: AppProps) {
	return (
		<WunderGraphRelayProvider initialRecords={pageProps.initialRecords}>
			<Component {...pageProps} />
		</WunderGraphRelayProvider>
	);
}

That's all. All you have to do is wrap your application with WunderGraphRelayProvider and pass prop initialRecords. This works with NextJS 12, 13, Vite and others, as it does not depend on framework-specific APIs.

Next, we need to configure the Relay compiler to work with WunderGraph. As you will see, WunderGraph and Relay are a match made in heaven. Both are built with the same principles in mind: declarativeness, type safety, default safety, locality.

Relay is the frontend counterpart to the WunderGraph backend. WunderGraph parses one or more GraphQL & REST APIs and represents them as a single GraphQL schema, which we call a virtual graph. Virtual because we don't actually expose this GraphQL schema to the outside world. Instead, we print it to a file to enable autocompletion in the IDE and make it available to the Relay compiler.

At runtime, we do not expose the GraphQL API to the outside world. Instead, we only provide an RPC API that allows the client to perform pre-registered GraphQL operations. The architecture of both WunderGraph and Relay makes integration seamless.

It seems that WunderGraph is the missing server counterpart to Relay.

Relay compiler configuration with support for stored operations out of the box

So how do we enable the Relay compiler to work with WunderGraph?

As mentioned above, WunderGraph automatically saves all GraphQL operations during the build phase. In order for this to work, we need to tell the Relay compiler where to “store” the stored operations. On the other hand, Relay needs to know where to find the GraphQL schema. Since WunderGraph stores the generated GraphQL schema in a file, all we need to do is connect them both using the section relay V package.json.

{
  "relay": {
    "src": "./src",
    "artifactDirectory": "./src/__relay__generated__",
    "language": "typescript",
    "schema": "./.wundergraph/generated/wundergraph.schema.graphql",
    "exclude": [
      "**/node_modules/**",
      "**/__mocks__/**",
      "**/__generated__/**",
      "**/.wundergraph/generated/**"
    ],
    "persistConfig": {
      "file": "./.wundergraph/operations/relay/persisted.json"
    },
    "eagerEsModules": true
  }
}

With this configuration, the Relay compiler will collect all GraphQL operations in the directory ./srcgenerate TypeScript types and store saved operations in ./.wundergraph/operations/relay/persisted.json. Each stored operation is a pair of a unique ID (hash) and a GraphQL operation. WunderGraph will automatically read this file, expand it in .graphql files and save them in ./.wundergraph/operations/relay/which will automatically register them as JSON-RPC endpoints.

Additionally, the WunderGraph code generator will generate for you WunderGraphRelayEnvironmentwhich internally implements fetch to make RPC calls to the WunderGraph API.

Here's a shortened version of the internals:

const fetchQuery: FetchFunction = async (params, variables) => {
		const { id, operationKind } = params;
		const response =
			operationKind === 'query'
				? await client.query({
						operationName: `relay/${id}`,
						input: variables,
				  })
				: await client.mutate({
						operationName: `relay/${id}`,
						input: variables,
				  });
		return {
			...response,
			errors: response.error ? [response.error] : [],
		};
	};

Function fetchQuery creates JSON-RPC requests from operation IDs and variables, no GraphQL is involved at this stage.

Server Side Rendering (SSR) with NextJS, Relay and WunderGraph

Now that we've set up the Relay compiler, we can start integrating Relay into our NextJS application, such as with server-side rendering (SSR).

import { graphql } from 'react-relay';
import { pagesDragonsQuery as PagesDragonsQueryType } from '../__relay__generated__/pagesDragonsQuery.graphql';
import { Dragon } from '@/components/Dragon';
import { fetchWunderGraphSSRQuery } from '@/lib/wundergraph';
import { InferGetServerSidePropsType } from 'next';

const PagesDragonsQuery = graphql`
	query pagesDragonsQuery {
		spacex_dragons {
			...Dragons_display_details
		}
	}
`;

export async function getServerSideProps() {
	const relayData = await fetchWunderGraphSSRQuery<PagesDragonsQueryType>(PagesDragonsQuery);

	return {
		props: relayData,
	};
}

export default function Home({ queryResponse }: InferGetServerSidePropsType<typeof getServerSideProps>) {
    return (
        <div>
            {queryResponse.spacex_dragons.map((dragon) => (
                <Dragon key={dragon.id} dragon={dragon} />
            ))}
        </div>
    );
}

Render as you get with Vite, Relay and WunderGraph

Here's another example of using Vite with rendering as data is received:

import { graphql, loadQuery } from 'react-relay';
import { getEnvironment, useLiveQuery } from '../lib/wundergraph';
import { Dragon } from './Dragon';
import { DragonsListDragonsQuery as DragonsListDragonsQueryType } from '../__relay__generated__/DragonsListDragonsQuery.graphql';

const AppDragonsQuery = graphql`
	query DragonsListDragonsQuery {
		spacex_dragons {
			...Dragons_display_details
		}
	}
`;

const dragonsListQueryReference = loadQuery<DragonsListDragonsQueryType>(getEnvironment(), AppDragonsQuery, {});

export const DragonsList = () => {
	const { data } = useLiveQuery<DragonsListDragonsQueryType>({
		query: AppDragonsQuery,
		queryReference: dragonsListQueryReference,
	});

	return (
		<div>
			<p>Dragons:</p>
			{data?.spacex_dragons?.map((dragon, dragonIndex) => {
				if (dragon) return <Dragon key={dragonIndex.toString()} dragon={dragon} />;
				return null;
			})}
		</div>
	);
};

Conclusion

Your key takeaway should be that GraphQL and Relay bring a lot of value. With WunderGraph, you can build modern, rich applications based on three strong pillars:

  • Placement of components and data requirements

  • Separated reusable components using data masking

  • Compile-time validation and security

Moreover, with this stack you are truly not limited to just the GraphQL API and React. It's possible to use Relay with a REST API, or even SOAP, and we're not limited to React either, since Relay is just a data fetching library.

If you want to learn more about WunderGraph, check out documentation.

Want to try some examples?

One more thing. This is truly just the beginning of our journey to making the power of GraphQL and Relay available to everyone. Stay connected at Twitter or join our community Discordto stay updated as we'll be launching something really exciting soon that will take this to the next level.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *