Skip to content

Commit

Permalink
Update withApollo example (#10451)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
HaNdTriX and Timer authored Feb 13, 2020
1 parent 4617950 commit 1a50d99
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 97 deletions.
18 changes: 18 additions & 0 deletions examples/with-apollo/apolloClient.js
Original file line number Diff line number Diff line change
@@ -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),
})
}
184 changes: 98 additions & 86 deletions examples/with-apollo/lib/apollo.js
Original file line number Diff line number Diff line change
@@ -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} />
Expand All @@ -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 = {}
Expand All @@ -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
Expand All @@ -92,68 +141,31 @@ 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.
// Handle them in components via the data.error prop:
// 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),
})
}
12 changes: 6 additions & 6 deletions examples/with-apollo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion examples/with-apollo/pages/about.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import App from '../components/App'
import Header from '../components/Header'

export default () => (
const AboutPage = () => (
<App>
<Header />
<article>
Expand Down Expand Up @@ -41,3 +41,5 @@ export default () => (
</article>
</App>
)

export default AboutPage
3 changes: 1 addition & 2 deletions examples/with-apollo/pages/client-only.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
4 changes: 2 additions & 2 deletions examples/with-apollo/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -26,4 +26,4 @@ const IndexPage = props => (
</App>
)

export default withApollo()(IndexPage)
export default withApollo({ ssr: true })(IndexPage)

0 comments on commit 1a50d99

Please sign in to comment.