Created
November 26, 2019 22:37
-
-
Save maraisr/515ccaa3d04ab7ce6eea969578f5db49 to your computer and use it in GitHub Desktop.
Next.js + Relay + TypeScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
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
@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