This article is really a GitHub issue that was fetched by Relay through OneGraph and turned into an article by Next.js
It uses getStaticProps
to deliver a statically-generated version of the article. That way search engines and people with javascript disabled can see the content.
There may be new comments or reactions since the last time Next.js ran getStaticProps
, so React hydrates the page on the client and we re-render with the latest version of the data fetched from OneGraph.
Our getStaticProps
function runs the query for the article and serializes the Relay environment to JSON, which is what gets used to statically render the page.
import {fetchQuery} from 'react-relay/hooks';
export async function getStaticProps(context) {
const issueNumber = context.params.number;
const environment = createRelayEnvironment();
await fetchQuery(environment, articleQuery, {issueNumber}).toPromise();
return {
revalidate: 600,
props: {
issueNumber,
initialRecords: environment
.getStore()
.getSource()
.toJSON(),
},
};
}
Then we put the initialRecords into the environment when we hydrate the page on the client.
export const Article = ({issueNumber, initialRecords}) => {
const environment = useRelayEnvironment();
// Only add cached records on initial render
React.useMemo(() => {
if (initialRecords) {
environment.getStore().publish(new RecordSource(initialRecords));
}
}, [initialRecords]);
const data = useLazyLoadQuery(
articleQuery,
{issueNumber},
// store-and-network will populate `data` with records from the store and
// fetch new data from the network in the background
{fetchPolicy: 'store-and-network'},
);
return <Article issue={data.gitHub.repository.issue} />
};
The 'store-and-network'
fetchPolicy
tells Relay to render with data from the store initially, then send off a network request to get the latest data.
Relay won’t tell you if the data that you’re rendering with is from the network or from the cache. It’s a difficult problem for Relay to solve generally. In many cases, it’s not even clear what it means for data to be cached. Relay has a central store that all queries add data to. It would be difficult and expensive to keep track of the provenance of every piece of data in the store.
For our use-case, the question is more straightforward. Was the data added to the store by publishing our cached initialRecords
, or through the fetch that useLazyLoadQuery
initiated?
We can answer that question by leveraging client-only fields and custom handlers.
Relay allows you to extend your server schema with client-only fields. This is typically used for local state management, and can replace tools like redux depending on the use case.
We’ll add a client-only field to the GitHubIssue
type by extending the type in a new file called src/clientSchema.graphl
.
extend type GitHubIssue {
isClientFetched: Boolean
}
Then we’ll add the field to our query and specify that it should use a custom handler with the @__clientField
directive
query ArticleQuery {
gitHub {
repository(name: $name, owner: $owner) {
issue(number: $issueNumber) {
isClientFetched @__clientField(handle: "isClientFetched")
...ArticleFragment
}
}
}
}
The @__clientField
directive takes a handle
argument, which is just a string we will use to determine which handler to use.
Handlers are extension points that allow for custom logic to add data to the store. Relay has default handlers built-in to deal with things like Connection
types, a pattern many GraphQL services use for pagination.
We’ll write our own handler for isClientFetched
and use it when we construct the environment, with a fallback to the default handler:
import {DefaultHandlerProvider} from 'relay-runtime';
const isClientFetchedHandler = {
update(store /*: RecordSourceProxy */, payload /*: HandleFieldPayload*/) {
const record = store.get(payload.dataID);
if (!record) return;
const isClient = typeof window !== 'undefined';
// value first, then key. Confusing, but I'm sure there's a good reason for it
record.setValue(isClient, payload.handleKey);
},
};
function handlerProvider(handle /*: string */) /*: Handler */ {
switch (handle) {
case 'isClientFetched':
return isClientFetchedHandler;
default:
return DefaultHandlerProvider(handle);
}
}
const environment = new Environment({
handlerProvider,
network: Network.create(fetchQuery)
});
The handler will run when a fetch, like the one triggered by useLazyLoadQuery
, adds data to the store, but not when we add records directly to the store with store.publish
.
On the server, the isClientFetched
field will be set to false. It will only become true when we fetch the query on the client.
The full code for this is in the OneBlog repo. We use isClientFetched
on OneBlog to determine if we should show a welcome page to the author before they have published any articles.
You can deploy your own blog backed by GitHub issues on Vercel.
Test
@dwwoelfel
why is
useMemo
used here instead ofuseEffect
?also instead of
i am using
is there a performance difference between
commitPayload
&environment.getStore().publish()
seems like with publish i am calling the network more frequently