Update withApollo example (#10451)

* Make withApollo work with _app.js components

* Support wrapping functional _App

* Add apolloClient to NextPageContext & NextPageContext

* Propertly call App.getInitialProps if used in NextAppContext

* Add Automatic Static Optimization warning

* Update deps

* Reduce API surface

* Move back to singleton client

* Improve documentation

* Remove Head.rewind()
We can get rid of .rewind by now as the latest next/head no longer uses legacy context.

* Add extra docs

* Reuse apolloState coming from previous hocs

Co-authored-by: Joe Haddad <timer150@gmail.com>
This commit is contained in:
Henrik Wenz 2020-02-13 06:26:22 +01:00 committed by GitHub
parent 4617950457
commit 1a50d99fbd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 128 additions and 97 deletions

View file

@ -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),
})
}

View file

@ -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 (
<ApolloProvider client={client}>
<PageComponent {...pageProps} />
@ -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(<AppTree {...props} />)
} 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),
})
}

View file

@ -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",

View file

@ -1,7 +1,7 @@
import App from '../components/App'
import Header from '../components/Header'
export default () => (
const AboutPage = () => (
<App>
<Header />
<article>
@ -41,3 +41,5 @@ export default () => (
</article>
</App>
)
export default AboutPage

View file

@ -26,5 +26,4 @@ const ClientOnlyPage = props => (
</App>
)
// Disable apollo ssr fetching in favour of automatic static optimization
export default withApollo({ ssr: false })(ClientOnlyPage)
export default withApollo()(ClientOnlyPage)

View file

@ -5,7 +5,7 @@ import Submit from '../components/Submit'
import PostList from '../components/PostList'
import { withApollo } from '../lib/apollo'
const IndexPage = props => (
const IndexPage = () => (
<App>
<Header />
<InfoBox>
@ -26,4 +26,4 @@ const IndexPage = props => (
</App>
)
export default withApollo()(IndexPage)
export default withApollo({ ssr: true })(IndexPage)