Created
March 27, 2019 16:46
-
-
Save elliottsj/610df8153a8805891646f0aabff1c911 to your computer and use it in GitHub Desktop.
Composable Next.js App HoCs with TypeScript: appWithCookies + appWithApolloClient
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 ApolloClient from 'apollo-client'; | |
import { NormalizedCacheObject } from 'apollo-cache-inmemory'; | |
import { NextContext } from 'next'; | |
import App, { Container, NextAppContext, AppProps } from 'next/app'; | |
import { DefaultQuery } from 'next/router'; | |
import * as React from 'react'; | |
import { ApolloProvider } from 'react-apollo'; | |
import { Cookies, CookiesProvider } from 'react-cookie'; | |
import appWithApolloClient from './appWithApolloClient'; | |
import appWithCookies from './appWithCookies'; | |
export interface NextAppInitialProps { | |
pageProps: any; | |
} | |
interface MyAppParams { | |
apolloClient: ApolloClient<NormalizedCacheObject>; | |
cookies: Cookies; | |
} | |
type MyAppProps = AppProps & MyAppParams; | |
type MyAppContext = NextAppContext & MyAppParams; | |
export type MyAppPageContext<Q extends DefaultQuery = DefaultQuery> = NextContext<Q> & MyAppParams; | |
export class MyApp extends App<MyAppProps> { | |
static async getInitialProps({ | |
ctx, | |
Component, | |
apolloClient, | |
cookies, | |
}: MyAppContext): Promise<NextAppInitialProps> { | |
let pageProps; | |
if (Component.getInitialProps) { | |
const c: MyAppPageContext = { ...ctx, apolloClient, cookies }; | |
pageProps = await Component.getInitialProps(c); | |
} else { | |
pageProps = {}; | |
} | |
return { pageProps }; | |
} | |
render() { | |
const { Component, apolloClient, pageProps, cookies } = this.props; | |
return ( | |
<Container> | |
<ApolloProvider client={apolloClient}> | |
<CookiesProvider cookies={cookies}> | |
<Component {...pageProps} /> | |
</CookiesProvider> | |
</ApolloProvider> | |
</Container> | |
); | |
} | |
} | |
export default appWithApolloClient(appWithCookies(MyApp)); |
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
/** | |
* React higher-order component (HoC) which wraps the App component and: | |
* - Performs the page's initial GraphQL request on the server, and dehydrates the result to be used | |
* as the initial Apollo state once the client mounts. | |
* - Passes the Apollo client to the wrapped App component. | |
* | |
* See also: | |
* - https://reactjs.org/docs/higher-order-components.html | |
* - https://github.com/zeit/next.js/blob/1babde1026a89b538d2caebd5d74ef6351871566/examples/with-apollo/lib/with-apollo-client.js | |
*/ | |
import ApolloClient, { ApolloError } from 'apollo-client'; | |
import { NormalizedCacheObject } from 'apollo-cache-inmemory'; | |
import { NextComponentType } from 'next'; | |
import { NextAppContext, AppProps } from 'next/app'; | |
import Head from 'next/head'; | |
import * as React from 'react'; | |
import { getDataFromTree } from 'react-apollo'; | |
import initApollo from './initApollo'; | |
export interface ApolloNetworkError extends Error { | |
result?: { | |
errors: ApolloNetworkErrorReason[]; | |
}; | |
} | |
export interface ApolloNetworkErrorReason { | |
extensions: any; | |
locations: object[]; | |
message: string; | |
} | |
export function isApolloError(err: Error): err is ApolloError { | |
return err.hasOwnProperty('graphQLErrors'); | |
} | |
const isBrowser = typeof window !== 'undefined'; | |
/** | |
* Props which must be returned by the Next.js App component's getInitialProps() method. | |
*/ | |
export interface NextAppInitialProps { | |
pageProps: any; | |
} | |
export interface AppWithApolloClientInitialProps<TWrappedAppInitialProps> { | |
apolloState: NormalizedCacheObject; | |
pageProps: any; | |
wrappedAppInitialProps: TWrappedAppInitialProps; | |
} | |
/** | |
* Additional parameters passed by AppWithApolloClient to WrappedApp. | |
*/ | |
interface AppWithApolloClientParams { | |
apolloClient: ApolloClient<NormalizedCacheObject>; | |
} | |
/** | |
* @template TWrappedAppParams | |
* The parameters which WrappedApp expects via props _and_ getInitialProps context. For example, | |
* WrappedApp may expect a `cookies` parameter as `ctx.cookies` and `props.cookies`. | |
* @template TWrappedAppInitialProps | |
* The initial props returned by WrappedApp.getInitialProps(). By default, this is NextAppInitialProps, | |
* but may be extended by WrappedApp. | |
*/ | |
const appWithApolloClient = < | |
TWrappedAppParams extends object = {}, | |
TWrappedAppInitialProps extends NextAppInitialProps = NextAppInitialProps | |
>( | |
WrappedApp: NextComponentType< | |
AppProps & TWrappedAppParams & TWrappedAppInitialProps & AppWithApolloClientParams, | |
TWrappedAppInitialProps, | |
NextAppContext & TWrappedAppParams & AppWithApolloClientParams | |
>, | |
) => { | |
const wrappedComponentName = WrappedApp.displayName || WrappedApp.name || 'Component'; | |
class AppWithApolloClient extends React.Component< | |
AppProps & TWrappedAppParams & AppWithApolloClientInitialProps<TWrappedAppInitialProps> | |
> { | |
static displayName = `appWithApolloClient(${wrappedComponentName})`; | |
static async getInitialProps( | |
ctx: NextAppContext & TWrappedAppParams, | |
): Promise<AppWithApolloClientInitialProps<TWrappedAppInitialProps>> { | |
const { Component, router } = ctx; | |
const apolloClient = initApollo(); | |
let wrappedAppInitialProps; | |
if (WrappedApp.getInitialProps) { | |
const wrappedAppCtx: NextAppContext & TWrappedAppParams & AppWithApolloClientParams = { | |
...ctx, | |
apolloClient, | |
}; | |
wrappedAppInitialProps = await WrappedApp.getInitialProps(wrappedAppCtx); | |
} else { | |
// If `WrappedApp.getInitialProps` is not defined, force WrappedApp to accept empty | |
// pageProps as its initial props: | |
wrappedAppInitialProps = { pageProps: {} } as TWrappedAppInitialProps; | |
} | |
if (!isBrowser) { | |
// Run all GraphQL queries in the component tree | |
// and extract the resulting data | |
try { | |
// Run all GraphQL queries | |
const waParams: TWrappedAppParams = ctx; | |
await getDataFromTree( | |
<WrappedApp | |
{...wrappedAppInitialProps} | |
{...waParams} | |
Component={Component} | |
router={router} | |
apolloClient={apolloClient} | |
/>, | |
); | |
} catch (error) { | |
// Prevent Apollo Client GraphQL errors from crashing SSR. | |
// Handle them in components via the data.error prop: | |
// https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-options | |
if (isApolloError(error) && error.networkError) { | |
const networkError: ApolloNetworkError = error.networkError; | |
if (!networkError.result) { | |
console.error('Error while running `getDataFromTree`', networkError); | |
} else { | |
const networkErrorReason = networkError.result.errors[0]; | |
console.error('Error while running `getDataFromTree`', networkErrorReason.message); | |
} | |
} else { | |
console.error('Error while running `getDataFromTree`', error); | |
} | |
} | |
// getDataFromTree does not call componentWillUnmount, | |
// Head side effect therefore need to be cleared manually. | |
// See https://github.com/gaearon/react-side-effect to learn more about why this is necessary. | |
Head.rewind(); | |
} | |
return { | |
// Extract query data from the Apollo store | |
apolloState: apolloClient.cache.extract(), | |
pageProps: wrappedAppInitialProps ? wrappedAppInitialProps.pageProps : {}, | |
wrappedAppInitialProps: wrappedAppInitialProps, | |
}; | |
} | |
private apolloClient: ApolloClient<NormalizedCacheObject>; | |
constructor( | |
props: AppProps & | |
TWrappedAppParams & | |
AppWithApolloClientInitialProps<TWrappedAppInitialProps>, | |
) { | |
super(props); | |
// `getDataFromTree` renders the component first, the client is passed off as a property. | |
// After that rendering is done using Next's normal rendering pipeline | |
this.apolloClient = initApollo(props.apolloState); | |
} | |
render() { | |
const { wrappedAppInitialProps } = this.props; | |
// TODO: remove the following type assertion once proper spread types are implemented: | |
// https://github.com/Microsoft/TypeScript/issues/10727 | |
const waProps: AppProps & TWrappedAppParams = this.props as AppProps & TWrappedAppParams; | |
const waiProps: TWrappedAppInitialProps = wrappedAppInitialProps; | |
return <WrappedApp {...waProps} {...waiProps} apolloClient={this.apolloClient} />; | |
} | |
} | |
const AWC: NextComponentType< | |
AppProps & TWrappedAppParams & AppWithApolloClientInitialProps<TWrappedAppInitialProps>, | |
AppWithApolloClientInitialProps<TWrappedAppInitialProps>, | |
NextAppContext & TWrappedAppParams | |
> = AppWithApolloClient; | |
return AWC; | |
}; | |
export default appWithApolloClient; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment