Skip to content

Instantly share code, notes, and snippets.

@maraisr
Created November 26, 2019 22:37
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save maraisr/515ccaa3d04ab7ce6eea969578f5db49 to your computer and use it in GitHub Desktop.
Save maraisr/515ccaa3d04ab7ce6eea969578f5db49 to your computer and use it in GitHub Desktop.
Next.js + Relay + TypeScript
import fetch from 'isomorphic-unfetch';
import React, { createContext, useContext, useMemo } from 'react';
import { ReactRelayContext } from 'react-relay';
import {
Environment,
FetchFunction,
Network,
RecordSource,
RequestParameters,
Store,
Variables,
} from 'relay-runtime';
import { config } from '../config';
const fetchQuery: FetchFunction = async (
request: RequestParameters,
variables: Variables,
) => {
const response: Response = await fetch(
`${config.api.gatewayUrl}/graph/graphql`,
{
method: 'POST',
cache: 'no-cache',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'x-api-key': config.api.key,
},
body: JSON.stringify({
query: request.text,
variables,
doc_id: request.id,
}),
},
);
const data = await response.json();
if (response.status >= 400) {
throw data.errors;
}
if (isMutation(request) && data.errors) {
throw data;
}
if (!data.data) {
throw data.errors;
}
return data;
};
const network = Network.create(fetchQuery);
const createEnvironment = (
records: ConstructorParameters<typeof RecordSource>[0] = {},
): Environment => {
const source = new RecordSource(records);
const store = new Store(source);
return new Environment({
network,
store,
});
};
let memoEnv: Environment = null;
export const getEnvironment = (
records: ConstructorParameters<typeof RecordSource>[0] = {},
) => {
if (process.browser && memoEnv === null) {
memoEnv = createEnvironment(records);
return memoEnv;
}
if (process.browser) {
return memoEnv;
}
return createEnvironment(records);
};
const isMutation = (request: RequestParameters) =>
request.operationKind === 'mutation';
const EnvironmentContext = createContext<Environment>(null);
export const EnvironmentProvider = ({
children,
initRecords,
initVariables,
}) => {
const environment = useMemo(() => getEnvironment(initRecords), [
initRecords,
]);
const relayContextInit = useMemo(
() => ({ environment, variables: initVariables }),
[environment, initVariables],
);
return (
<EnvironmentContext.Provider value={environment}>
<ReactRelayContext.Provider value={relayContextInit}>
{children}
</ReactRelayContext.Provider>
</EnvironmentContext.Provider>
);
};
export const useEnvironment = (): Environment => useContext(EnvironmentContext);
import { NextPage, NextPageContext } from 'next';
import Error from 'next/error';
import React, { ComponentType } from 'react';
import { fetchQuery, graphql } from 'react-relay';
import { OperationType } from 'relay-runtime';
import { EnvironmentProvider, getEnvironment } from './environment';
interface DefaultProps {
hasError?: boolean;
statusCode?: number;
}
interface Options<Query extends OperationType, Props> {
query: ReturnType<typeof graphql>;
variables?: (ctx: NextPageContext) => Promise<Query['variables']>;
getInitialProps?: (
ctx: NextPageContext,
query: Query,
) => Promise<Omit<Props, 'records' | 'query'> & DefaultProps>;
}
export function withData<Query extends OperationType, Props extends {} = {}>(
ComposedComponent: ComponentType<Query>,
options: Options<Query, Props>,
) {
const WithDataComponent: NextPage<{
query: Query;
records: any;
} & DefaultProps> = props => {
return props.hasError ? (
<Error statusCode={props.statusCode} />
) : (
<EnvironmentProvider
initRecords={props.records}
initVariables={props.query.variables}>
<ComposedComponent {...props.query} />
</EnvironmentProvider>
);
};
WithDataComponent.getInitialProps = async ctx => {
const environment = getEnvironment();
const variables: Query['variables'] = options.variables
? await options.variables(ctx)
: {};
const queryProps: Query['response'] = await fetchQuery(
environment,
options.query,
variables,
);
const queryRecords = environment
.getStore()
.getSource()
.toJSON();
// @ts-ignore
const query: Query = {
response: queryProps,
variables,
} as const;
const { statusCode = 200, hasError = false, ...otherProps } =
typeof options.getInitialProps === 'function'
? await options.getInitialProps(ctx, query)
: {};
if (!process.browser && hasError && ctx) {
// eslint-disable-next-line require-atomic-updates
ctx.res.statusCode = statusCode;
ctx.res.setHeader(
'Cache-Control',
'no-cache, no-store, must-revalidate',
);
ctx.res.setHeader('Pragma', 'no-cache');
ctx.res.setHeader('Expires', -1);
}
return {
query,
records: queryRecords,
hasError,
statusCode,
...otherProps,
} as const;
};
return WithDataComponent;
}
@tony
Copy link

tony commented Feb 16, 2020

@maraisr Any example of how you would use withData? Is this from a GH issue?

Is it / can you license it for reuse on MIT license/whatever license relay uses?

@maraisr
Copy link
Author

maraisr commented Feb 16, 2020

Hi @tony, yeah by all means use it. Im working on a new better one which I can share with you also, this particular one has some flaws. So if you wanted to check back in about a week, I can share it with you!

It's largely influence by this: https://dev.to/marais/relay-and-ssr-using-createoperationdescriptor-k6f

@tony
Copy link

tony commented Feb 17, 2020

@maraisr yeah definitely and thank you for the link. I'm on day #2 with next.js

Interested in how the implementation looks and what the deployment looks like. I'm using next.js with a graphene (django) website atm and getting it on SSR

@qarthandgi
Copy link

Hello @maraisr, thank you for putting this together! Do you have any other resource that has a complete example of this working. Even though I'm well-acquainted with Next & React & Apollo, putting Typescript, Next, and Relay together seemed to be the perfect storm.

Looking for a comprehensive example if you've finished the one you were talking about! Really appreciate it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment