[Examples] Move with-apollo-and-redux to SSG (#13779)
Ref: https://github.com/vercel/next.js/pull/13742
This commit is contained in:
parent
06ac0adcf8
commit
3a9fbd8396
10 changed files with 204 additions and 272 deletions
|
@ -1,31 +1,33 @@
|
|||
import Link from 'next/link'
|
||||
import { withRouter } from 'next/router'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const Nav = ({ router: { pathname } }) => (
|
||||
<header>
|
||||
<Link href="/">
|
||||
<a className={pathname === '/' ? 'is-active' : ''}>Home</a>
|
||||
</Link>
|
||||
<Link href="/apollo">
|
||||
<a className={pathname === '/apollo' ? 'is-active' : ''}>Apollo</a>
|
||||
</Link>
|
||||
<Link href="/redux">
|
||||
<a className={pathname === '/redux' ? 'is-active' : ''}>Redux</a>
|
||||
</Link>
|
||||
<style jsx>{`
|
||||
header {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
a {
|
||||
font-size: 14px;
|
||||
margin-right: 15px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.is-active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`}</style>
|
||||
</header>
|
||||
)
|
||||
export default function Nav() {
|
||||
const { pathname } = useRouter()
|
||||
|
||||
export default withRouter(Nav)
|
||||
return (
|
||||
<header>
|
||||
<Link href="/">
|
||||
<a className={pathname === '/' ? 'is-active' : ''}>Home</a>
|
||||
</Link>
|
||||
<Link href="/apollo">
|
||||
<a className={pathname === '/apollo' ? 'is-active' : ''}>Apollo</a>
|
||||
</Link>
|
||||
<Link href="/redux">
|
||||
<a className={pathname === '/redux' ? 'is-active' : ''}>Redux</a>
|
||||
</Link>
|
||||
<style jsx>{`
|
||||
header {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
a {
|
||||
font-size: 14px;
|
||||
margin-right: 15px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.is-active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`}</style>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,134 +1,38 @@
|
|||
import Head from 'next/head'
|
||||
import { ApolloProvider } from '@apollo/react-hooks'
|
||||
import { useMemo } from 'react'
|
||||
import { ApolloClient } from 'apollo-client'
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory'
|
||||
import { HttpLink } from 'apollo-link-http'
|
||||
|
||||
let globalApolloClient = null
|
||||
let apolloClient
|
||||
|
||||
/**
|
||||
* Creates and provides the apolloContext
|
||||
* to a next.js PageTree. Use it by wrapping
|
||||
* your PageComponent via HOC pattern.
|
||||
* @param {Function|Class} PageComponent
|
||||
* @param {Object} [config]
|
||||
* @param {Boolean} [config.ssr=true]
|
||||
*/
|
||||
export function withApollo(PageComponent, { ssr = true } = {}) {
|
||||
const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
|
||||
const client = apolloClient || initApolloClient(apolloState)
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<PageComponent {...pageProps} />
|
||||
</ApolloProvider>
|
||||
)
|
||||
}
|
||||
|
||||
// Set the correct displayName in development
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const displayName =
|
||||
PageComponent.displayName || PageComponent.name || 'Component'
|
||||
|
||||
if (displayName === 'App') {
|
||||
console.warn('This withApollo HOC only works with PageComponents.')
|
||||
}
|
||||
|
||||
WithApollo.displayName = `withApollo(${displayName})`
|
||||
}
|
||||
|
||||
if (ssr || PageComponent.getInitialProps) {
|
||||
WithApollo.getInitialProps = async (ctx) => {
|
||||
const { AppTree } = ctx
|
||||
|
||||
// Initialize ApolloClient, add it to the ctx object so
|
||||
// we can use it in `PageComponent.getInitialProp`.
|
||||
const apolloClient = (ctx.apolloClient = initApolloClient())
|
||||
|
||||
// Run wrapped getInitialProps methods
|
||||
let pageProps = {}
|
||||
if (PageComponent.getInitialProps) {
|
||||
pageProps = await PageComponent.getInitialProps(ctx)
|
||||
}
|
||||
|
||||
// Only on the server:
|
||||
if (typeof window === 'undefined') {
|
||||
// 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) {
|
||||
try {
|
||||
// Run all GraphQL queries
|
||||
const { getDataFromTree } = await import('@apollo/react-ssr')
|
||||
await getDataFromTree(
|
||||
<AppTree
|
||||
pageProps={{
|
||||
...pageProps,
|
||||
apolloClient,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return WithApollo
|
||||
}
|
||||
|
||||
/**
|
||||
* Always creates a new apollo client on the server
|
||||
* Creates or reuses apollo client in the browser.
|
||||
* @param {Object} initialState
|
||||
*/
|
||||
function 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={}]
|
||||
*/
|
||||
function createApolloClient(initialState = {}) {
|
||||
// Check out https://github.com/vercel/next.js/pull/4611 if you want to use the AWSAppSyncClient
|
||||
function createApolloClient() {
|
||||
return new ApolloClient({
|
||||
ssrMode: typeof window === 'undefined', // Disables forceFetch on the server (so queries are only run once)
|
||||
ssrMode: typeof window === 'undefined',
|
||||
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`
|
||||
}),
|
||||
cache: new InMemoryCache().restore(initialState),
|
||||
cache: new InMemoryCache(),
|
||||
})
|
||||
}
|
||||
|
||||
export function initializeApollo(initialState = null) {
|
||||
const _apolloClient = apolloClient ?? createApolloClient()
|
||||
|
||||
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
|
||||
// get hydrated here
|
||||
if (initialState) {
|
||||
_apolloClient.cache.restore(initialState)
|
||||
}
|
||||
// For SSG and SSR always create a new Apollo Client
|
||||
if (typeof window === 'undefined') return _apolloClient
|
||||
// Create the Apollo Client once in the client
|
||||
if (!apolloClient) apolloClient = _apolloClient
|
||||
|
||||
return _apolloClient
|
||||
}
|
||||
|
||||
export function useApollo(initialState) {
|
||||
const store = useMemo(() => initializeApollo(initialState), [initialState])
|
||||
return store
|
||||
}
|
||||
|
|
|
@ -1,71 +1,74 @@
|
|||
import { Provider } from 'react-redux'
|
||||
import { initializeStore } from '../store'
|
||||
import App from 'next/app'
|
||||
import { useMemo } from 'react'
|
||||
import { createStore, applyMiddleware } from 'redux'
|
||||
import { composeWithDevTools } from 'redux-devtools-extension'
|
||||
|
||||
export const withRedux = (PageComponent, { ssr = true } = {}) => {
|
||||
const WithRedux = ({ initialReduxState, ...props }) => {
|
||||
const store = getOrInitializeStore(initialReduxState)
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<PageComponent {...props} />
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
let store
|
||||
|
||||
// Make sure people don't use this HOC on _app.js level
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const isAppHoc =
|
||||
PageComponent === App || PageComponent.prototype instanceof App
|
||||
if (isAppHoc) {
|
||||
throw new Error('The withRedux HOC only works with PageComponents')
|
||||
}
|
||||
}
|
||||
const initialState = {
|
||||
lastUpdate: 0,
|
||||
light: false,
|
||||
count: 0,
|
||||
}
|
||||
|
||||
// Set the correct displayName in development
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const displayName =
|
||||
PageComponent.displayName || PageComponent.name || 'Component'
|
||||
|
||||
WithRedux.displayName = `withRedux(${displayName})`
|
||||
}
|
||||
|
||||
if (ssr || PageComponent.getInitialProps) {
|
||||
WithRedux.getInitialProps = async (context) => {
|
||||
// Get or Create the store with `undefined` as initialState
|
||||
// This allows you to set a custom default initialState
|
||||
const reduxStore = getOrInitializeStore()
|
||||
|
||||
// Provide the store to getInitialProps of pages
|
||||
context.reduxStore = reduxStore
|
||||
|
||||
// Run getInitialProps from HOCed PageComponent
|
||||
const pageProps =
|
||||
typeof PageComponent.getInitialProps === 'function'
|
||||
? await PageComponent.getInitialProps(context)
|
||||
: {}
|
||||
|
||||
// Pass props to PageComponent
|
||||
const reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case 'TICK':
|
||||
return {
|
||||
...pageProps,
|
||||
initialReduxState: reduxStore.getState(),
|
||||
...state,
|
||||
lastUpdate: action.lastUpdate,
|
||||
light: !!action.light,
|
||||
}
|
||||
}
|
||||
case 'INCREMENT':
|
||||
return {
|
||||
...state,
|
||||
count: state.count + 1,
|
||||
}
|
||||
case 'DECREMENT':
|
||||
return {
|
||||
...state,
|
||||
count: state.count - 1,
|
||||
}
|
||||
case 'RESET':
|
||||
return {
|
||||
...state,
|
||||
count: initialState.count,
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
||||
return WithRedux
|
||||
}
|
||||
|
||||
let reduxStore
|
||||
const getOrInitializeStore = (initialState) => {
|
||||
// Always make a new store if server, otherwise state is shared between requests
|
||||
if (typeof window === 'undefined') {
|
||||
return initializeStore(initialState)
|
||||
}
|
||||
|
||||
// Create store if unavailable on the client and set it on the window object
|
||||
if (!reduxStore) {
|
||||
reduxStore = initializeStore(initialState)
|
||||
}
|
||||
|
||||
return reduxStore
|
||||
function initStore(preloadedState = initialState) {
|
||||
return createStore(
|
||||
reducer,
|
||||
preloadedState,
|
||||
composeWithDevTools(applyMiddleware())
|
||||
)
|
||||
}
|
||||
|
||||
export const initializeStore = (preloadedState) => {
|
||||
let _store = store ?? initStore(preloadedState)
|
||||
|
||||
// After navigating to a page with an initial Redux state, merge that state
|
||||
// with the current state in the store, and create a new store
|
||||
if (preloadedState && store) {
|
||||
_store = initStore({
|
||||
...store.getState(),
|
||||
...preloadedState,
|
||||
})
|
||||
// Reset the current store
|
||||
store = undefined
|
||||
}
|
||||
|
||||
// For SSG and SSR always create a new store
|
||||
if (typeof window === 'undefined') return _store
|
||||
// Create the store once in the client
|
||||
if (!store) store = _store
|
||||
|
||||
return _store
|
||||
}
|
||||
|
||||
export function useStore(initialState) {
|
||||
const store = useMemo(() => initializeStore(initialState), [initialState])
|
||||
return store
|
||||
}
|
||||
|
|
|
@ -3,9 +3,11 @@ import { useEffect, useRef } from 'react'
|
|||
// https://overreacted.io/making-setinterval-declarative-with-react-hooks/
|
||||
const useInterval = (callback, delay) => {
|
||||
const savedCallback = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback
|
||||
}, [callback])
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (...args) => savedCallback.current(...args)
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"react": "^16.11.0",
|
||||
"react-dom": "^16.11.0",
|
||||
"react-redux": "^7.1.1",
|
||||
"redux": "^4.0.1"
|
||||
"redux": "^4.0.1",
|
||||
"redux-devtools-extension": "2.13.8"
|
||||
}
|
||||
}
|
||||
|
|
17
examples/with-apollo-and-redux/pages/_app.js
Normal file
17
examples/with-apollo-and-redux/pages/_app.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { ApolloProvider } from '@apollo/react-hooks'
|
||||
import { Provider } from 'react-redux'
|
||||
import { useStore } from '../lib/redux'
|
||||
import { useApollo } from '../lib/apollo'
|
||||
|
||||
export default function App({ Component, pageProps }) {
|
||||
const store = useStore(pageProps.initialReduxState)
|
||||
const apolloClient = useApollo(pageProps.initialApolloState)
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<ApolloProvider client={apolloClient}>
|
||||
<Component {...pageProps} />
|
||||
</ApolloProvider>
|
||||
</Provider>
|
||||
)
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
import { initializeApollo } from '../lib/apollo'
|
||||
import Layout from '../components/Layout'
|
||||
import Submit from '../components/Submit'
|
||||
import PostList from '../components/PostList'
|
||||
import { withApollo } from '../lib/apollo'
|
||||
import PostList, {
|
||||
ALL_POSTS_QUERY,
|
||||
allPostsQueryVars,
|
||||
} from '../components/PostList'
|
||||
|
||||
const ApolloPage = () => (
|
||||
<Layout>
|
||||
|
@ -10,4 +13,20 @@ const ApolloPage = () => (
|
|||
</Layout>
|
||||
)
|
||||
|
||||
export default withApollo(ApolloPage)
|
||||
export async function getStaticProps() {
|
||||
const apolloClient = initializeApollo()
|
||||
|
||||
await apolloClient.query({
|
||||
query: ALL_POSTS_QUERY,
|
||||
variables: allPostsQueryVars,
|
||||
})
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialApolloState: apolloClient.cache.extract(),
|
||||
},
|
||||
unstable_revalidate: 1,
|
||||
}
|
||||
}
|
||||
|
||||
export default ApolloPage
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
import { useDispatch } from 'react-redux'
|
||||
import { withRedux } from '../lib/redux'
|
||||
import { compose } from 'redux'
|
||||
import { withApollo } from '../lib/apollo'
|
||||
import { initializeStore } from '../lib/redux'
|
||||
import { initializeApollo } from '../lib/apollo'
|
||||
import useInterval from '../lib/useInterval'
|
||||
import Layout from '../components/Layout'
|
||||
import Clock from '../components/Clock'
|
||||
import Counter from '../components/Counter'
|
||||
import Submit from '../components/Submit'
|
||||
import PostList from '../components/PostList'
|
||||
import PostList, {
|
||||
ALL_POSTS_QUERY,
|
||||
allPostsQueryVars,
|
||||
} from '../components/PostList'
|
||||
|
||||
const IndexPage = () => {
|
||||
// Tick the time every second
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useInterval(() => {
|
||||
dispatch({
|
||||
type: 'TICK',
|
||||
|
@ -19,6 +22,7 @@ const IndexPage = () => {
|
|||
lastUpdate: Date.now(),
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
{/* Redux */}
|
||||
|
@ -32,17 +36,29 @@ const IndexPage = () => {
|
|||
)
|
||||
}
|
||||
|
||||
IndexPage.getInitialProps = ({ reduxStore }) => {
|
||||
// Tick the time once, so we'll have a
|
||||
// valid time before first render
|
||||
export async function getStaticProps() {
|
||||
const reduxStore = initializeStore()
|
||||
const apolloClient = initializeApollo()
|
||||
const { dispatch } = reduxStore
|
||||
|
||||
dispatch({
|
||||
type: 'TICK',
|
||||
light: typeof window === 'object',
|
||||
light: true,
|
||||
lastUpdate: Date.now(),
|
||||
})
|
||||
|
||||
return {}
|
||||
await apolloClient.query({
|
||||
query: ALL_POSTS_QUERY,
|
||||
variables: allPostsQueryVars,
|
||||
})
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialReduxState: reduxStore.getState(),
|
||||
initialApolloState: apolloClient.cache.extract(),
|
||||
},
|
||||
unstable_revalidate: 1,
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(withApollo, withRedux)(IndexPage)
|
||||
export default IndexPage
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useDispatch } from 'react-redux'
|
||||
import { withRedux } from '../lib/redux'
|
||||
import { initializeStore } from '../lib/redux'
|
||||
import useInterval from '../lib/useInterval'
|
||||
import Layout from '../components/Layout'
|
||||
import Clock from '../components/Clock'
|
||||
|
@ -8,6 +8,7 @@ import Counter from '../components/Counter'
|
|||
const ReduxPage = () => {
|
||||
// Tick the time every second
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useInterval(() => {
|
||||
dispatch({
|
||||
type: 'TICK',
|
||||
|
@ -15,6 +16,7 @@ const ReduxPage = () => {
|
|||
lastUpdate: Date.now(),
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Clock />
|
||||
|
@ -23,17 +25,22 @@ const ReduxPage = () => {
|
|||
)
|
||||
}
|
||||
|
||||
ReduxPage.getInitialProps = ({ reduxStore }) => {
|
||||
// Tick the time once, so we'll have a
|
||||
// valid time before first render
|
||||
export async function getStaticProps() {
|
||||
const reduxStore = initializeStore()
|
||||
const { dispatch } = reduxStore
|
||||
|
||||
dispatch({
|
||||
type: 'TICK',
|
||||
light: typeof window === 'object',
|
||||
light: true,
|
||||
lastUpdate: Date.now(),
|
||||
})
|
||||
|
||||
return {}
|
||||
return {
|
||||
props: {
|
||||
initialReduxState: reduxStore.getState(),
|
||||
},
|
||||
unstable_revalidate: 1,
|
||||
}
|
||||
}
|
||||
|
||||
export default withRedux(ReduxPage)
|
||||
export default ReduxPage
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
import { createStore } from 'redux'
|
||||
|
||||
const initialState = {
|
||||
lastUpdate: 0,
|
||||
light: false,
|
||||
count: 0,
|
||||
}
|
||||
|
||||
const reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case 'TICK':
|
||||
return {
|
||||
...state,
|
||||
lastUpdate: action.lastUpdate,
|
||||
light: !!action.light,
|
||||
}
|
||||
case 'INCREMENT':
|
||||
return {
|
||||
...state,
|
||||
count: state.count + 1,
|
||||
}
|
||||
case 'DECREMENT':
|
||||
return {
|
||||
...state,
|
||||
count: state.count - 1,
|
||||
}
|
||||
case 'RESET':
|
||||
return {
|
||||
...state,
|
||||
count: initialState.count,
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export const initializeStore = (preloadedState = initialState) => {
|
||||
return createStore(reducer, preloadedState)
|
||||
}
|
Loading…
Reference in a new issue