Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update withApollo example #10451

Merged
merged 16 commits into from Feb 13, 2020
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 18 additions & 0 deletions 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),
})
}
184 changes: 98 additions & 86 deletions 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I did this in next-with-apollo a long time ago, I don't remember why I never put it here 🤣

Copy link
Member

@lfades lfades Feb 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, is this required? I did it as a bug fix for a configuration in the package, but I don't know about this case

Copy link
Member

@lfades lfades Feb 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reason is, it feels very hacky, the overhead of creating a new instance of the Apollo Client may not matter (otherwise go ahead)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy for this to be fixed in a follow-up! It's likely fixing an already existing bug.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lfades This line is there to prevent the following issue.

Screenshot 2020-02-15 at 11 44 20

I think it is a genius hack that reduces a lot of complexity inside this HOC.
I have to admit that this was not my idea. I believe I saw this pattern inside your excellent lfades/next-with-apollo module, the first time.


// 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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to @timneutkens , this should not have been removed: #9326 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for clarifying 🙏. #10696

}
}

// 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
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
@@ -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
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
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)