[Examples] Move with-apollo-and-redux to SSG (#13779)

Ref: https://github.com/vercel/next.js/pull/13742
This commit is contained in:
Luis Alvarez D 2020-06-09 16:11:45 -05:00 committed by GitHub
parent 06ac0adcf8
commit 3a9fbd8396
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 204 additions and 272 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View 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>
)
}

View file

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

View file

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

View file

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

View file

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