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

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

Merged
merged 8 commits into from
Jun 9, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 30 additions & 28 deletions examples/with-apollo-and-redux/components/Nav.js
Original file line number Diff line number Diff line change
@@ -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>
)
}
146 changes: 25 additions & 121 deletions examples/with-apollo-and-redux/lib/apollo.js
Original file line number Diff line number Diff line change
@@ -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
function createApolloClient() {
return new ApolloClient({
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(),
})
}

/**
* 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)
}
export function initializeApollo(initialState = null) {
const _apolloClient = apolloClient ?? createApolloClient()

// Reuse client on the client-side
if (!globalApolloClient) {
globalApolloClient = createApolloClient(initialState)
// 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 globalApolloClient
return _apolloClient
}

/**
* 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
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`
}),
cache: new InMemoryCache().restore(initialState),
})
export function useApollo(initialState) {
const store = useMemo(() => initializeApollo(initialState), [initialState])
return store
}
119 changes: 61 additions & 58 deletions examples/with-apollo-and-redux/lib/redux.js
Original file line number Diff line number Diff line change
@@ -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>
)
}

// 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')
}
}

// 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
let store

// Run getInitialProps from HOCed PageComponent
const pageProps =
typeof PageComponent.getInitialProps === 'function'
? await PageComponent.getInitialProps(context)
: {}
const initialState = {
lastUpdate: 0,
light: false,
count: 0,
}

// Pass props to PageComponent
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 {
...pageProps,
initialReduxState: reduxStore.getState(),
...state,
count: initialState.count,
}
}
default:
return state
}
}

return WithRedux
function initStore(preloadedState = initialState) {
return createStore(
reducer,
preloadedState,
composeWithDevTools(applyMiddleware())
)
}

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)
}
export const initializeStore = (preloadedState) => {
let _store = store ?? initStore(preloadedState)

// Create store if unavailable on the client and set it on the window object
if (!reduxStore) {
reduxStore = initializeStore(initialState)
// 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
}

return reduxStore
// 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
}
2 changes: 2 additions & 0 deletions examples/with-apollo-and-redux/lib/useInterval.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 2 additions & 1 deletion examples/with-apollo-and-redux/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 17 additions & 0 deletions examples/with-apollo-and-redux/pages/_app.js
Original file line number Diff line number Diff line change
@@ -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>
)
}
Loading