From 8d3262500dd5b888c4c8d44d849110838c87011e Mon Sep 17 00:00:00 2001 From: Jayden Seric Date: Mon, 10 Jul 2017 13:59:49 +1000 Subject: [PATCH] Apollo decorator refactor. --- app/helpers/get-display-name.js | 8 ++ app/helpers/init-apollo-client.js | 37 +++++++++ app/helpers/with-data.js | 133 +++++++++++++++++------------- 3 files changed, 121 insertions(+), 57 deletions(-) create mode 100644 app/helpers/get-display-name.js create mode 100644 app/helpers/init-apollo-client.js diff --git a/app/helpers/get-display-name.js b/app/helpers/get-display-name.js new file mode 100644 index 0000000..790fa62 --- /dev/null +++ b/app/helpers/get-display-name.js @@ -0,0 +1,8 @@ +/** + * Gets the display name of a JSX component for dev tools. + * @param {Object | Function} component - A JSX component. + * @returns {String} The component display name. + */ +export default function getDisplayName({ displayName, name }) { + return displayName ? displayName : name && name !== '' ? name : 'Unknown' +} diff --git a/app/helpers/init-apollo-client.js b/app/helpers/init-apollo-client.js new file mode 100644 index 0000000..ad597d8 --- /dev/null +++ b/app/helpers/init-apollo-client.js @@ -0,0 +1,37 @@ +import { ApolloClient } from 'react-apollo' +import { createNetworkInterface } from 'apollo-upload-client' +import 'isomorphic-fetch' + +// Used in the browser to share a single Apollo Client instance between +// decorated components. +let apolloClient = null + +/** + * Creates a new Apollo Client instance. + * @param {Object} [initialState] - Apollo client Redux store initial state. + * @returns {Object} Apollo Client instance. + */ +const createApolloClient = initialState => + new ApolloClient({ + initialState, + ssrMode: !process.browser, + networkInterface: createNetworkInterface({ + uri: process.env.API_URI + }) + }) + +/** + * Gets or creates the Apollo Client instance. + * @param {Object} [initialState] - Apollo client Redux store initial state. + * @returns {Object} Apollo Client instance. + */ +export default function initApolloClient(initialState) { + // Create a new client every server-side request so that data isn't shared + // between connections. + if (!process.browser) return createApolloClient(initialState) + + // Reuse client on the client-side. + if (!apolloClient) apolloClient = createApolloClient(initialState) + + return apolloClient +} diff --git a/app/helpers/with-data.js b/app/helpers/with-data.js index b4a3d25..1c376db 100644 --- a/app/helpers/with-data.js +++ b/app/helpers/with-data.js @@ -1,73 +1,92 @@ -import 'isomorphic-fetch' -import React from 'react' -import { ApolloClient, ApolloProvider, getDataFromTree } from 'react-apollo' -import { createNetworkInterface } from 'apollo-upload-client' +import { Component } from 'react' +import { ApolloProvider, getDataFromTree } from 'react-apollo' +import Head from 'next/head' +import initApolloClient from './init-apollo-client' +import getDisplayName from './get-display-name' -const ssrMode = !process.browser -let apolloClient = null +export default ComposedComponent => { + return class WithData extends Component { + static displayName = `WithData(${getDisplayName(ComposedComponent)})` -function initClient(headers, initialState) { - return new ApolloClient({ - initialState, - ssrMode, - networkInterface: createNetworkInterface({ - uri: process.env.API_URI - }) - }) -} - -function getClient(headers, initialState = {}) { - if (ssrMode) return initClient(headers, initialState) - if (!apolloClient) apolloClient = initClient(headers, initialState) - return apolloClient -} - -export default Component => - class extends React.Component { - static async getInitialProps(ctx) { - const headers = ctx.req ? ctx.req.headers : {} - const client = getClient(headers) - - const props = { - url: { - query: ctx.query, - pathname: ctx.pathname - }, - ...(await (Component.getInitialProps - ? Component.getInitialProps(ctx) - : {})) - } - - if (ssrMode) { - const app = ( - - - - ) - await getDataFromTree(app) - } - - return { - initialState: { - apollo: { - data: client.getInitialState().data + /** + * Gets the initial props for a Next.js page component. + * Executes on the server for the initial page load. Executes on the client + * when navigating to a different route via the Link component or using the + * routing APIs. For either environment the initial props returned must be + * serializable to JSON. + * @see https://github.com/zeit/next.js/issues/978 + * @see https://github.com/zeit/next.js/#fetching-data-and-component-lifecycle + * @param {Object} context + * @param {String} context.pathname - Path section of the page URL. + * @param {Object} context.query - Query string section of the page URL parsed as an object. + * @param {Object} context.req - HTTP request (server only). + * @param {Object} context.res - HTTP response (server only). + * @param {Object} context.jsonPageRes - Fetch Response (client only). + * @param {Object} context.err - Error encountered during the rendering, if any. + * @returns {Promise} Page component props. + */ + static async getInitialProps(context) { + const initialProps = { + composedComponentProps: { + url: { + query: context.query, + pathname: context.pathname } - }, - headers, - ...props + } } + + // If the page component has initial props, merge them in. + if (ComposedComponent.getInitialProps) { + Object.assign( + initialProps.composedComponentProps, + await ComposedComponent.getInitialProps(context) + ) + } + + if (!process.browser) { + const apolloClient = initApolloClient() + + try { + // Recurse the component tree and prefetch all Apollo data queries to + // populate the Apollo Client Redux store. This allows an instant + // server side render. + // See: http://dev.apollodata.com/react/server-side-rendering.html#getDataFromTree + await getDataFromTree( + + + + ) + } catch (error) { + // Prevent Apollo Client GraphQL errors from crashing SSR. + // Handle them in components via the data.error prop: + // http://dev.apollodata.com/react/api-queries.html#graphql-query-data-error + } + + // Forget Head items found during the getDataFromTree render to prevent + // duplicates in the real render. + Head.rewind() + + // Set Apollo Client initial state so the client can adopt data fetched + // on the server. + initialProps.initialState = { + apollo: apolloClient.getInitialState() + } + } + + return initialProps } constructor(props) { super(props) - this.client = getClient(this.props.headers, this.props.initialState) + this.apolloClient = initApolloClient(this.props.initialState) } render() { return ( - - + + ) } } +}