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:
parent
4617950457
commit
1a50d99fbd
6 changed files with 128 additions and 97 deletions
18
examples/with-apollo/apolloClient.js
Normal file
18
examples/with-apollo/apolloClient.js
Normal 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),
|
||||
})
|
||||
}
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue