From 10d2055833363499056d4f7c9b16a3a96c7dc157 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Wed, 3 Jun 2020 23:03:19 -0500 Subject: [PATCH 1/2] Initial changes --- .../with-apollo-and-redux/components/Nav.js | 62 ++++---- examples/with-apollo-and-redux/lib/apollo.js | 146 +++--------------- .../lib/getApolloState.js | 27 ++++ examples/with-apollo-and-redux/lib/redux.js | 119 +++++++------- .../with-apollo-and-redux/lib/useInterval.js | 2 + examples/with-apollo-and-redux/package.json | 3 +- examples/with-apollo-and-redux/pages/_app.js | 17 ++ examples/with-apollo-and-redux/pages/index.js | 24 +-- examples/with-apollo-and-redux/store.js | 39 ----- 9 files changed, 183 insertions(+), 256 deletions(-) create mode 100644 examples/with-apollo-and-redux/lib/getApolloState.js create mode 100644 examples/with-apollo-and-redux/pages/_app.js delete mode 100644 examples/with-apollo-and-redux/store.js diff --git a/examples/with-apollo-and-redux/components/Nav.js b/examples/with-apollo-and-redux/components/Nav.js index fea1086c4e0eb..0290916ddc663 100644 --- a/examples/with-apollo-and-redux/components/Nav.js +++ b/examples/with-apollo-and-redux/components/Nav.js @@ -1,31 +1,37 @@ import Link from 'next/link' -import { withRouter } from 'next/router' +import { useRouter } from 'next/router' -const Nav = ({ router: { pathname } }) => ( -
- - Home - - - Apollo - - - Redux - - -
-) +const Nav = () => { + const router = useRouter() + console.log('YO', router) + const { pathname } = router -export default withRouter(Nav) + return ( +
+ + Home + + + Apollo + + + Redux + + +
+ ) +} + +export default Nav diff --git a/examples/with-apollo-and-redux/lib/apollo.js b/examples/with-apollo-and-redux/lib/apollo.js index 67ceba2219fbe..77ee7a59b1149 100644 --- a/examples/with-apollo-and-redux/lib/apollo.js +++ b/examples/with-apollo-and-redux/lib/apollo.js @@ -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 ( - - - - ) - } - - // 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( - - ) - } 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 } diff --git a/examples/with-apollo-and-redux/lib/getApolloState.js b/examples/with-apollo-and-redux/lib/getApolloState.js new file mode 100644 index 0000000000000..88e0f7e9ea03b --- /dev/null +++ b/examples/with-apollo-and-redux/lib/getApolloState.js @@ -0,0 +1,27 @@ +import { Provider } from 'react-redux' +import { ApolloProvider } from '@apollo/react-hooks' +import { getDataFromTree } from '@apollo/react-ssr' +import { initializeApollo } from './apollo' +import { initializeStore } from './redux' + +export default async function getApolloState( + Page, + reduxStore = initializeStore() +) { + const apolloClient = initializeApollo() + + // Take a Next.js page, determine which queries are needed to render, + // and fetch them. This method can be pretty slow since it renders + // your entire page 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( + + + + + + ) + + return apolloClient.cache.extract() +} diff --git a/examples/with-apollo-and-redux/lib/redux.js b/examples/with-apollo-and-redux/lib/redux.js index 00c5f09a55d0d..69432e4d552a4 100644 --- a/examples/with-apollo-and-redux/lib/redux.js +++ b/examples/with-apollo-and-redux/lib/redux.js @@ -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 ( - - - - ) - } - - // 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 } diff --git a/examples/with-apollo-and-redux/lib/useInterval.js b/examples/with-apollo-and-redux/lib/useInterval.js index 066d08ee254b5..0bfb631018d0d 100644 --- a/examples/with-apollo-and-redux/lib/useInterval.js +++ b/examples/with-apollo-and-redux/lib/useInterval.js @@ -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) diff --git a/examples/with-apollo-and-redux/package.json b/examples/with-apollo-and-redux/package.json index f7ff3e4bc4339..59e3a0d9acce2 100644 --- a/examples/with-apollo-and-redux/package.json +++ b/examples/with-apollo-and-redux/package.json @@ -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" } } diff --git a/examples/with-apollo-and-redux/pages/_app.js b/examples/with-apollo-and-redux/pages/_app.js new file mode 100644 index 0000000000000..82525867b8d5c --- /dev/null +++ b/examples/with-apollo-and-redux/pages/_app.js @@ -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 ( + + + + + + ) +} diff --git a/examples/with-apollo-and-redux/pages/index.js b/examples/with-apollo-and-redux/pages/index.js index 3493a1f205da2..977a30f5f4263 100644 --- a/examples/with-apollo-and-redux/pages/index.js +++ b/examples/with-apollo-and-redux/pages/index.js @@ -1,7 +1,6 @@ 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 getApolloState from '../lib/getApolloState' import useInterval from '../lib/useInterval' import Layout from '../components/Layout' import Clock from '../components/Clock' @@ -12,6 +11,7 @@ import PostList from '../components/PostList' const IndexPage = () => { // Tick the time every second const dispatch = useDispatch() + useInterval(() => { dispatch({ type: 'TICK', @@ -19,6 +19,7 @@ const IndexPage = () => { lastUpdate: Date.now(), }) }, 1000) + return ( {/* Redux */} @@ -32,17 +33,22 @@ 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 { dispatch } = reduxStore + + // If you build and start the app, the date added here will have the same + // value for all requests, as this method gets executed at build time. dispatch({ type: 'TICK', - light: typeof window === 'object', + light: true, lastUpdate: Date.now(), }) - return {} + const initialApolloState = await getApolloState(IndexPage, reduxStore) + const initialReduxState = reduxStore.getState() + + return { props: { initialApolloState, initialReduxState } } } -export default compose(withApollo, withRedux)(IndexPage) +export default IndexPage diff --git a/examples/with-apollo-and-redux/store.js b/examples/with-apollo-and-redux/store.js deleted file mode 100644 index 991a33452dc20..0000000000000 --- a/examples/with-apollo-and-redux/store.js +++ /dev/null @@ -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) -} From 32e2addabd27135e8337e831b459a0b62370fb0a Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Thu, 4 Jun 2020 18:27:06 -0500 Subject: [PATCH 2/2] Updated implementation --- .../with-apollo-and-redux/components/Nav.js | 8 ++---- .../lib/getApolloState.js | 27 ------------------- .../with-apollo-and-redux/pages/apollo.js | 25 ++++++++++++++--- examples/with-apollo-and-redux/pages/index.js | 24 ++++++++++++----- examples/with-apollo-and-redux/pages/redux.js | 21 ++++++++++----- 5 files changed, 55 insertions(+), 50 deletions(-) delete mode 100644 examples/with-apollo-and-redux/lib/getApolloState.js diff --git a/examples/with-apollo-and-redux/components/Nav.js b/examples/with-apollo-and-redux/components/Nav.js index 0290916ddc663..ad328e93b6193 100644 --- a/examples/with-apollo-and-redux/components/Nav.js +++ b/examples/with-apollo-and-redux/components/Nav.js @@ -1,10 +1,8 @@ import Link from 'next/link' import { useRouter } from 'next/router' -const Nav = () => { - const router = useRouter() - console.log('YO', router) - const { pathname } = router +export default function Nav() { + const { pathname } = useRouter() return (
@@ -33,5 +31,3 @@ const Nav = () => {
) } - -export default Nav diff --git a/examples/with-apollo-and-redux/lib/getApolloState.js b/examples/with-apollo-and-redux/lib/getApolloState.js deleted file mode 100644 index 88e0f7e9ea03b..0000000000000 --- a/examples/with-apollo-and-redux/lib/getApolloState.js +++ /dev/null @@ -1,27 +0,0 @@ -import { Provider } from 'react-redux' -import { ApolloProvider } from '@apollo/react-hooks' -import { getDataFromTree } from '@apollo/react-ssr' -import { initializeApollo } from './apollo' -import { initializeStore } from './redux' - -export default async function getApolloState( - Page, - reduxStore = initializeStore() -) { - const apolloClient = initializeApollo() - - // Take a Next.js page, determine which queries are needed to render, - // and fetch them. This method can be pretty slow since it renders - // your entire page 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( - - - - - - ) - - return apolloClient.cache.extract() -} diff --git a/examples/with-apollo-and-redux/pages/apollo.js b/examples/with-apollo-and-redux/pages/apollo.js index d597a6043d03c..7d19b38bcd4f2 100644 --- a/examples/with-apollo-and-redux/pages/apollo.js +++ b/examples/with-apollo-and-redux/pages/apollo.js @@ -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 = () => ( @@ -10,4 +13,20 @@ const ApolloPage = () => ( ) -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 diff --git a/examples/with-apollo-and-redux/pages/index.js b/examples/with-apollo-and-redux/pages/index.js index 977a30f5f4263..d4b28defabd49 100644 --- a/examples/with-apollo-and-redux/pages/index.js +++ b/examples/with-apollo-and-redux/pages/index.js @@ -1,12 +1,15 @@ import { useDispatch } from 'react-redux' import { initializeStore } from '../lib/redux' -import getApolloState from '../lib/getApolloState' +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 @@ -35,20 +38,27 @@ const IndexPage = () => { export async function getStaticProps() { const reduxStore = initializeStore() + const apolloClient = initializeApollo() const { dispatch } = reduxStore - // If you build and start the app, the date added here will have the same - // value for all requests, as this method gets executed at build time. dispatch({ type: 'TICK', light: true, lastUpdate: Date.now(), }) - const initialApolloState = await getApolloState(IndexPage, reduxStore) - const initialReduxState = reduxStore.getState() + await apolloClient.query({ + query: ALL_POSTS_QUERY, + variables: allPostsQueryVars, + }) - return { props: { initialApolloState, initialReduxState } } + return { + props: { + initialReduxState: reduxStore.getState(), + initialApolloState: apolloClient.cache.extract(), + }, + unstable_revalidate: 1, + } } export default IndexPage diff --git a/examples/with-apollo-and-redux/pages/redux.js b/examples/with-apollo-and-redux/pages/redux.js index 0ed8f2e97d6b8..6b90c1cdbc533 100644 --- a/examples/with-apollo-and-redux/pages/redux.js +++ b/examples/with-apollo-and-redux/pages/redux.js @@ -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 ( @@ -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