Next.js 13 with Relay
If you run into any problems following step by step guide, check files in this repo.
npx create-next-app@latest --experimental-app next-13-relay
Here,
next-13-relay
is the name of the app you're creating
Need to install the following packages:
create-next-app@latest
Ok to proceed? (y) y
✔ Would you like to use TypeScript with this project? … No / Yes
✔ Would you like to use ESLint with this project? … No / Yes
Reply yes
to prompts
Success! Created next-13-relay at </parent/directory>/next-13-relay
cd next-13-relay
npm install --save relay-runtime@main react-relay@main
npm install --save-dev relay-compiler@main @types/react-relay @types/relay-runtime
relay.config.json
{
"root": ".",
"sources": {
"src": "my_app",
"app": "my_app"
},
"projects": {
"my_app": {
"language": "typescript",
"schema": "schema.graphql",
"output": "__generated__",
"useImportTypeSyntax": true,
"featureFlags": {
"relay_resolver_model_syntax_enabled": true,
"use_named_imports_for_relay_resolvers": true,
"enable_relay_resolver_transform": true,
"relay_resolver_enable_terse_syntax": true
},
"eagerEsModules": true
}
}
}
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
compiler: {
relay: {
src: "./",
language: "typescript",
artifactDirectory: "__generated__",
},
},
experimental: { appDir: true },
};
module.exports = nextConfig;
Here we're using GitHub's graphql API, you can download GitHub's schema using
curl https://raw.githubusercontent.com/roshanjossey/nextjs-13-relay/main/schema.graphql > schema.graphql
Create an src
directory and a directory named relay
inside it (src/relay/
)
Create the following files inside relay
directory
src/relay/environment.ts
import {
Environment,
Network,
RecordSource,
Store,
RequestParameters,
QueryResponseCache,
Variables,
GraphQLResponse,
CacheConfig,
} from "relay-runtime";
const HTTP_ENDPOINT = "https://api.github.com/graphql";
const IS_SERVER = typeof window === typeof undefined;
const CACHE_TTL = 5 * 1000; // 5 seconds, to resolve preloaded results
export async function networkFetch(
request: RequestParameters,
variables: Variables
): Promise<GraphQLResponse> {
const token = process.env.NEXT_PUBLIC_REACT_APP_GITHUB_AUTH_TOKEN;
if (token == null || token === "") {
throw new Error(
"This app requires a GitHub authentication token to be configured. See readme.md for setup details."
);
}
const resp = await fetch(HTTP_ENDPOINT, {
method: "POST",
headers: {
Accept: "application/json",
Authorization: `bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: request.text,
variables,
}),
});
const json = await resp.json();
// GraphQL returns exceptions (for example, a missing required variable) in the "errors"
// property of the response. If any exceptions occurred when processing the request,
// throw an error to indicate to the developer what went wrong.
if (Array.isArray(json.errors)) {
console.error(json.errors);
throw new Error(
`Error fetching GraphQL query '${
request.name
}' with variables '${JSON.stringify(variables)}': ${JSON.stringify(
json.errors
)}`
);
}
return json;
}
export const responseCache: QueryResponseCache | null = IS_SERVER
? null
: new QueryResponseCache({
size: 100,
ttl: CACHE_TTL,
});
function createNetwork() {
async function fetchResponse(
params: RequestParameters,
variables: Variables,
cacheConfig: CacheConfig
) {
const isQuery = params.operationKind === "query";
const cacheKey = params.id ?? params.cacheID;
const forceFetch = cacheConfig && cacheConfig.force;
if (responseCache != null && isQuery && !forceFetch) {
const fromCache = responseCache.get(cacheKey, variables);
if (fromCache != null) {
return Promise.resolve(fromCache);
}
}
return networkFetch(params, variables);
}
const network = Network.create(fetchResponse);
return network;
}
function createEnvironment() {
return new Environment({
network: createNetwork(),
store: new Store(RecordSource.create()),
isServer: IS_SERVER,
});
}
export const environment = createEnvironment();
export function getCurrentEnvironment() {
if (IS_SERVER) {
return createEnvironment();
}
return environment;
}
src/relay/loadSerializableQuery.ts
import {
GraphQLResponse,
OperationType,
RequestParameters,
VariablesOf,
} from "relay-runtime";
import { ConcreteRequest } from "relay-runtime/lib/util/RelayConcreteNode";
import { networkFetch } from "./environment";
export interface SerializablePreloadedQuery<
TRequest extends ConcreteRequest,
TQuery extends OperationType
> {
params: TRequest["params"];
variables: VariablesOf<TQuery>;
response: GraphQLResponse;
}
// Call into raw network fetch to get serializable GraphQL query response
// This response will be sent to the client to "warm" the QueryResponseCache
// to avoid the client fetches.
export default async function loadSerializableQuery<
TRequest extends ConcreteRequest,
TQuery extends OperationType
>(
params: RequestParameters,
variables: VariablesOf<TQuery>
): Promise<SerializablePreloadedQuery<TRequest, TQuery>> {
const response = await networkFetch(params, variables);
return {
params,
variables,
response,
};
}
src/relay/useSerializablePreloadedQuery.ts
// Convert preloaded query object (with raw GraphQL Response) into
// Relay's PreloadedQuery.
import { useMemo } from "react";
import { PreloadedQuery, PreloadFetchPolicy } from "react-relay";
import { ConcreteRequest, IEnvironment, OperationType } from "relay-runtime";
import { responseCache } from "./environment";
import { SerializablePreloadedQuery } from "./loadSerializableQuery";
// This hook convert serializable preloaded query
// into Relay's PreloadedQuery object.
// It is also writes this serializable preloaded query
// into QueryResponseCache, so we the network layer
// can use these cache results when fetching data
// in `usePreloadedQuery`.
export default function useSerializablePreloadedQuery<
TRequest extends ConcreteRequest,
TQuery extends OperationType
>(
environment: IEnvironment,
preloadQuery: SerializablePreloadedQuery<TRequest, TQuery>,
fetchPolicy: PreloadFetchPolicy = "store-or-network"
): PreloadedQuery<TQuery> {
useMemo(() => {
writePreloadedQueryToCache(preloadQuery);
}, [preloadQuery]);
return {
environment,
fetchKey: preloadQuery.params.id ?? preloadQuery.params.cacheID,
fetchPolicy,
isDisposed: false,
name: preloadQuery.params.name,
kind: "PreloadedQuery",
variables: preloadQuery.variables,
dispose: () => {
return;
},
};
}
function writePreloadedQueryToCache<
TRequest extends ConcreteRequest,
TQuery extends OperationType
>(preloadedQueryObject: SerializablePreloadedQuery<TRequest, TQuery>) {
const cacheKey =
preloadedQueryObject.params.id ?? preloadedQueryObject.params.cacheID;
responseCache?.set(
cacheKey,
preloadedQueryObject.variables,
preloadedQueryObject.response
);
}
Change app/page.tsx
as follows:
import loadSerializableQuery from "../src/relay/loadSerializableQuery";
import MainViewQueryNode, {
MainViewQuery,
} from "../__generated__/MainViewQuery.graphql";
import MainViewClientComponent from "./MainViewClientComponent";
const Page = async () => {
const preloadedQuery = await loadSerializableQuery<
typeof MainViewQueryNode,
MainViewQuery
>(MainViewQueryNode.params, {
owner: "firstcontributions",
name: "first-contributions",
});
return (
<div>
<MainViewClientComponent preloadedQuery={preloadedQuery} />
</div>
);
};
export default Page;
export const revalidate = 0;
Also add app/MainView.tsx
"use client";
import { Suspense } from "react";
import { SerializablePreloadedQuery } from "../src/relay/loadSerializableQuery";
import MainViewQueryNode, {
MainViewQuery,
} from "../__generated__/MainViewQuery.graphql";
import { getCurrentEnvironment } from "../src/relay/environment";
import { RelayEnvironmentProvider, graphql, PreloadedQuery, usePreloadedQuery } from "react-relay";
import useSerializablePreloadedQuery from "../src/relay/useSerializablePreloadedQuery";
import Link from "next/link";
const MainViewClientComponent = (props: {
preloadedQuery: SerializablePreloadedQuery<
typeof MainViewQueryNode,
MainViewQuery
>;
}) => {
const environment = getCurrentEnvironment();
const queryRef = useSerializablePreloadedQuery(
environment,
props.preloadedQuery
);
return (
<RelayEnvironmentProvider environment={environment}>
<Suspense fallback="Loading...">
<MainView queryRef={queryRef} />
</Suspense>
</RelayEnvironmentProvider>
);
};
function MainView(props: {
queryRef: PreloadedQuery<MainViewQuery>;
}) {
const data = usePreloadedQuery(
graphql`
query MainViewQuery($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
nameWithOwner
description
url
}
}
`,
props.queryRef
);
return (
<div>
<h1>
{data.repository?.nameWithOwner}
</h1>
<span>{data.repository?.description}</span>
<div>
<span><strong><Link href={data.repository?.url}>Link</Link></strong></span>
</div>
</div>
);
}
export default MainViewClientComponent;
In scripts in your package.json, add a script called relay
"scripts": {
...,
"relay": "relay-compiler"
},
Now, you can run yarn relay
from your console. That'll generate artifacts in __generated__
directory
src/relay/environment.ts
requires an environment variable NEXT_PUBLIC_REACT_APP_GITHUB_AUTH_TOKEN
for GitHub graphql API Auth.
Create .env
in the root of your project. Get a personal access token with your GitHub account
.env
NEXT_PUBLIC_REACT_APP_GITHUB_AUTH_TOKEN=<token you create above>
In your console, run npm run dev
You should see the app running in http://localhost:3000