Skip to content

Instantly share code, notes, and snippets.

@elliottsj
Created March 27, 2019 16:46
Show Gist options
  • Save elliottsj/610df8153a8805891646f0aabff1c911 to your computer and use it in GitHub Desktop.
Save elliottsj/610df8153a8805891646f0aabff1c911 to your computer and use it in GitHub Desktop.
Composable Next.js App HoCs with TypeScript: appWithCookies + appWithApolloClient
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));
/**
* 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;
/**
* React higher-order component (HoC) which wraps the App component and passes a cookies access
* object to the App component.
*
* See also:
* - https://reactjs.org/docs/higher-order-components.html
*/
import { NextComponentType } from 'next';
import { NextAppContext, AppProps } from 'next/app';
import * as React from 'react';
import { Cookies } from 'react-cookie';
/**
* Props which must be returned by the Next.js App component's getInitialProps() method.
*/
export interface NextAppInitialProps {
pageProps: any;
}
export interface AppWithCookiesInitialProps<TWrappedAppInitialProps> {
cookieHeader: String;
pageProps: any;
wrappedAppInitialProps: TWrappedAppInitialProps;
}
/**
* Additional parameters passed by AppWithCookies to WrappedApp.
*/
interface AppWithCookiesParams {
cookies: Cookies;
}
/**
* @template TWrappedAppParams
* The parameters which WrappedApp expects via props _and_ getInitialProps context. For example,
* WrappedApp may expect a `apolloClient` parameter as `ctx.apolloClient` and `props.apolloClient`.
* @template TWrappedAppInitialProps
* The initial props returned by WrappedApp.getInitialProps(). By default, this is NextAppInitialProps,
* but may be extended by WrappedApp.
*/
const appWithCookies = <
TWrappedAppParams extends object = {},
TWrappedAppInitialProps extends NextAppInitialProps = NextAppInitialProps
>(
WrappedApp: NextComponentType<
AppProps & TWrappedAppParams & TWrappedAppInitialProps & { cookies: Cookies },
TWrappedAppInitialProps,
NextAppContext & TWrappedAppParams & AppWithCookiesParams
>,
) => {
const wrappedComponentName = WrappedApp.displayName || WrappedApp.name || 'Component';
class AppWithCookies extends React.Component<
AppProps & TWrappedAppParams & AppWithCookiesInitialProps<TWrappedAppInitialProps>
> {
static displayName = `appWithCookies(${wrappedComponentName})`;
static async getInitialProps(
ctx: NextAppContext & TWrappedAppParams,
): Promise<AppWithCookiesInitialProps<TWrappedAppInitialProps>> {
const { req } = ctx.ctx;
let cookieHeader;
let cookies;
if (req && req.headers.cookie) {
cookieHeader = Array.isArray(req.headers.cookie)
? req.headers.cookie[0]
: req.headers.cookie;
cookies = new Cookies(cookieHeader);
} else {
cookieHeader = '';
cookies = new Cookies();
}
let wrappedAppInitialProps;
if (WrappedApp.getInitialProps) {
const wrappedAppCtx: NextAppContext & TWrappedAppParams & AppWithCookiesParams = {
...ctx,
cookies,
};
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;
}
return {
cookieHeader,
pageProps: wrappedAppInitialProps ? wrappedAppInitialProps.pageProps : {},
wrappedAppInitialProps: wrappedAppInitialProps,
};
}
render() {
const { cookieHeader, wrappedAppInitialProps, ...props } = 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 = props as AppProps & TWrappedAppParams;
const waiProps: TWrappedAppInitialProps = wrappedAppInitialProps;
return <WrappedApp {...waProps} {...waiProps} cookies={new Cookies(cookieHeader)} />;
}
}
const AWC: NextComponentType<
AppProps & TWrappedAppParams & AppWithCookiesInitialProps<TWrappedAppInitialProps>,
AppWithCookiesInitialProps<TWrappedAppInitialProps>,
NextAppContext & TWrappedAppParams
> = AppWithCookies;
return AWC;
};
export default appWithCookies;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment