diff --git a/examples/with-apollo/apolloClient.js b/examples/with-apollo/apolloClient.js new file mode 100644 index 000000000000000..6f48ea6e97187d1 --- /dev/null +++ b/examples/with-apollo/apolloClient.js @@ -0,0 +1,18 @@ +import { ApolloClient } from 'apollo-client' +import { InMemoryCache } from 'apollo-cache-inmemory' +import { HttpLink } from 'apollo-link-http' +import fetch from 'isomorphic-unfetch' + +export default function createApolloClient(initialState, ctx) { + // The `ctx` (NextPageContext) will only be present on the server. + // use it to extract auth headers (ctx.req) or similar. + return new ApolloClient({ + ssrMode: Boolean(ctx), + link: new HttpLink({ + uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn', // Server URL (must be absolute) + credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers` + fetch, + }), + cache: new InMemoryCache().restore(initialState), + }) +} diff --git a/examples/with-apollo/lib/apollo.js b/examples/with-apollo/lib/apollo.js index 61721bbf2d21174..d8ffd59ce831b81 100644 --- a/examples/with-apollo/lib/apollo.js +++ b/examples/with-apollo/lib/apollo.js @@ -1,22 +1,94 @@ import React from 'react' import App from 'next/app' -import Head from 'next/head' import { ApolloProvider } from '@apollo/react-hooks' -import { ApolloClient } from 'apollo-client' -import { InMemoryCache } from 'apollo-cache-inmemory' -import { HttpLink } from 'apollo-link-http' -import fetch from 'isomorphic-unfetch' +import createApolloClient from '../apolloClient' +// On the client we store the apollo client in the following variable +// this prevents the client from reinitializing between page transitions. let globalApolloClient = null /** - * Creates and provides the apolloContext - * to a next.js PageTree. Use it by wrapping - * your PageComponent via HOC pattern. + * Installes the apollo client on NextPageContext + * or NextAppContext. Useful if you want to use apolloClient + * inside getStaticProps, getStaticPaths or getServerProps + * @param {NextPageContext | NextAppContext} ctx */ -export const withApollo = ({ ssr = true } = {}) => PageComponent => { +export const initOnContext = ctx => { + const inAppContext = Boolean(ctx.ctx) + + // We consider installing `withApollo({ ssr: true })` on global App level + // as antipattern since it disables project wide Automatic Static Optimization. + if (process.env.NODE_ENV === 'development') { + if (inAppContext) { + console.warn( + 'Warning: You have opted-out of Automatic Static Optimization due to `withApollo` in `pages/_app`.\n' + + 'Read more: https://err.sh/next.js/opt-out-auto-static-optimization\n' + ) + } + } + + // Initialize ApolloClient if not already done + const apolloClient = + ctx.apolloClient || + initApolloClient(ctx.apolloState || {}, inAppContext ? ctx.ctx : ctx) + + // To avoid calling initApollo() twice in the server we send the Apollo Client as a prop + // to the component, otherwise the component would have to call initApollo() again but this + // time without the context, once that happens the following code will make sure we send + // the prop as `null` to the browser + apolloClient.toJSON = () => null + + // Add apolloClient to NextPageContext & NextAppContext + // This allows us to consume the apolloClient inside our + // custom `getInitialProps({ apolloClient })`. + ctx.apolloClient = apolloClient + if (inAppContext) { + ctx.ctx.apolloClient = apolloClient + } + + return ctx +} + +/** + * Always creates a new apollo client on the server + * Creates or reuses apollo client in the browser. + * @param {NormalizedCacheObject} initialState + * @param {NextPageContext} ctx + */ +const initApolloClient = (initialState, ctx) => { + // Make sure to create a new client for every server-side request so that data + // isn't shared between connections (which would be bad) + if (typeof window === 'undefined') { + return createApolloClient(initialState, ctx) + } + + // Reuse client on the client-side + if (!globalApolloClient) { + globalApolloClient = createApolloClient(initialState, ctx) + } + + return globalApolloClient +} + +/** + * Creates a withApollo HOC + * that provides the apolloContext + * to a next.js Page or AppTree. + * @param {Object} withApolloOptions + * @param {Boolean} [withApolloOptions.ssr=false] + * @returns {(PageComponent: ReactNode) => ReactNode} + */ +export const withApollo = ({ ssr = false } = {}) => PageComponent => { const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => { - const client = apolloClient || initApolloClient(apolloState) + let client + if (apolloClient) { + // Happens on: getDataFromTree & next.js ssr + client = apolloClient + } else { + // Happens on: next.js csr + client = initApolloClient(apolloState, undefined) + } + return ( @@ -28,38 +100,13 @@ export const withApollo = ({ ssr = true } = {}) => PageComponent => { if (process.env.NODE_ENV !== 'production') { const displayName = PageComponent.displayName || PageComponent.name || 'Component' - WithApollo.displayName = `withApollo(${displayName})` } if (ssr || PageComponent.getInitialProps) { WithApollo.getInitialProps = async ctx => { - const { AppTree } = ctx const inAppContext = Boolean(ctx.ctx) - - if (process.env.NODE_ENV === 'development') { - if (inAppContext) { - console.warn( - 'Warning: You have opted-out of Automatic Static Optimization due to `withApollo` in `pages/_app`.\n' + - 'Read more: https://err.sh/next.js/opt-out-auto-static-optimization\n' - ) - } - } - - if (ctx.apolloClient) { - throw new Error('Multiple instances of withApollo found.') - } - - // Initialize ApolloClient - const apolloClient = initApolloClient() - - // Add apolloClient to NextPageContext & NextAppContext - // This allows us to consume the apolloClient inside our - // custom `getInitialProps({ apolloClient })`. - ctx.apolloClient = apolloClient - if (inAppContext) { - ctx.ctx.apolloClient = apolloClient - } + const { apolloClient } = initOnContext(ctx) // Run wrapped getInitialProps methods let pageProps = {} @@ -71,16 +118,18 @@ export const withApollo = ({ ssr = true } = {}) => PageComponent => { // Only on the server: if (typeof window === 'undefined') { + const { AppTree } = ctx // When redirecting, the response is finished. // No point in continuing to render if (ctx.res && ctx.res.finished) { return pageProps } - // Only if ssr is enabled - if (ssr) { + // Only if dataFromTree is enabled + if (ssr && AppTree) { try { - // Run all GraphQL queries + // Import `@apollo/react-ssr` dynamically. + // We don't want to have this in our client bundle. const { getDataFromTree } = await import('@apollo/react-ssr') // Since AppComponents and PageComponents have different context types @@ -92,8 +141,11 @@ export const withApollo = ({ ssr = true } = {}) => PageComponent => { props = { pageProps: { ...pageProps, apolloClient } } } - // Takes React AppTree, determine which queries are needed to render, - // then fetche them all. + // Take the Next.js AppTree, determine which queries are needed to render, + // and fetch them. This method can be pretty slow since it renders + // your entire AppTree once for every query. Check out apollo fragments + // if you want to reduce the number of rerenders. + // https://www.apollographql.com/docs/react/data/fragments/ await getDataFromTree() } catch (error) { // Prevent Apollo Client GraphQL errors from crashing SSR. @@ -101,59 +153,19 @@ export const withApollo = ({ ssr = true } = {}) => PageComponent => { // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error console.error('Error while running `getDataFromTree`', error) } - - // getDataFromTree does not call componentWillUnmount - // head side effect therefore need to be cleared manually - Head.rewind() } } - // Extract query data from the Apollo store - const apolloState = apolloClient.cache.extract() - return { ...pageProps, - apolloState, + // Extract query data from the Apollo store + apolloState: apolloClient.cache.extract(), + // Provide the client for ssr. As soon as this payload + // gets JSON.stringified it will remove itself. + apolloClient: ctx.apolloClient, } } } return WithApollo } - -/** - * Always creates a new apollo client on the server - * Creates or reuses apollo client in the browser. - * @param {Object} initialState - */ -const initApolloClient = initialState => { - // Make sure to create a new client for every server-side request so that data - // isn't shared between connections (which would be bad) - if (typeof window === 'undefined') { - return createApolloClient(initialState) - } - - // Reuse client on the client-side - if (!globalApolloClient) { - globalApolloClient = createApolloClient(initialState) - } - - return globalApolloClient -} - -/** - * Creates and configures the ApolloClient - * @param {Object} [initialState={}] - */ -const createApolloClient = (initialState = {}) => { - // Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient - return new ApolloClient({ - ssrMode: typeof window === 'undefined', // Disables forceFetch on the server (so queries are only run once) - link: new HttpLink({ - uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn', // Server URL (must be absolute) - credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers` - fetch, - }), - cache: new InMemoryCache().restore(initialState), - }) -} diff --git a/examples/with-apollo/package.json b/examples/with-apollo/package.json index 3718e480056be64..82a42c03fb8a7db 100644 --- a/examples/with-apollo/package.json +++ b/examples/with-apollo/package.json @@ -7,13 +7,13 @@ "start": "next start" }, "dependencies": { - "@apollo/react-hooks": "3.0.0", - "@apollo/react-ssr": "3.0.0", - "apollo-cache-inmemory": "1.6.3", - "apollo-client": "2.6.4", - "apollo-link-http": "1.5.15", + "@apollo/react-hooks": "3.1.3", + "@apollo/react-ssr": "3.1.3", + "apollo-cache-inmemory": "1.6.5", + "apollo-client": "2.6.8", + "apollo-link-http": "1.5.16", "graphql": "^14.0.2", - "graphql-tag": "2.10.1", + "graphql-tag": "2.10.3", "isomorphic-unfetch": "^3.0.0", "next": "latest", "prop-types": "^15.6.2", diff --git a/examples/with-apollo/pages/about.js b/examples/with-apollo/pages/about.js index d8bbe18afcb3386..01290e995b08b46 100644 --- a/examples/with-apollo/pages/about.js +++ b/examples/with-apollo/pages/about.js @@ -1,7 +1,7 @@ import App from '../components/App' import Header from '../components/Header' -export default () => ( +const AboutPage = () => (
@@ -41,3 +41,5 @@ export default () => (
) + +export default AboutPage diff --git a/examples/with-apollo/pages/client-only.js b/examples/with-apollo/pages/client-only.js index 7fa37e3ca22b520..445e956d8d71c73 100644 --- a/examples/with-apollo/pages/client-only.js +++ b/examples/with-apollo/pages/client-only.js @@ -26,5 +26,4 @@ const ClientOnlyPage = props => ( ) -// Disable apollo ssr fetching in favour of automatic static optimization -export default withApollo({ ssr: false })(ClientOnlyPage) +export default withApollo()(ClientOnlyPage) diff --git a/examples/with-apollo/pages/index.js b/examples/with-apollo/pages/index.js index d40059d1a4620c2..93e8d42fecaac10 100644 --- a/examples/with-apollo/pages/index.js +++ b/examples/with-apollo/pages/index.js @@ -5,7 +5,7 @@ import Submit from '../components/Submit' import PostList from '../components/PostList' import { withApollo } from '../lib/apollo' -const IndexPage = props => ( +const IndexPage = () => (
@@ -26,4 +26,4 @@ const IndexPage = props => ( ) -export default withApollo()(IndexPage) +export default withApollo({ ssr: true })(IndexPage)