diff --git a/examples/with-apollo-and-cache-persist-typescript/.gitignore b/examples/with-apollo-and-cache-persist-typescript/.gitignore new file mode 100644 index 000000000000..1437c53f70bc --- /dev/null +++ b/examples/with-apollo-and-cache-persist-typescript/.gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel diff --git a/examples/with-apollo-and-cache-persist-typescript/README.md b/examples/with-apollo-and-cache-persist-typescript/README.md new file mode 100644 index 000000000000..390a58f51d44 --- /dev/null +++ b/examples/with-apollo-and-cache-persist-typescript/README.md @@ -0,0 +1,27 @@ +# Next.js with Apollo Client and Apollo Cache Persist example + +This example is based on [with-apollo](https://github.com/vercel/next.js/tree/canary/examples/with-apollo) example and shows how to integrate [Apollo GraphQL client](https://www.apollographql.com/docs/react) into Next.js application using TypeScript with addition of [Apollo Cache Persist](https://github.com/apollographql/apollo-cache-persist) package that enables client side persistance of queried data. + +## Preview + +Preview the example live on [StackBlitz](http://stackblitz.com/): + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/with-apollo-and-cache-persist-typescript) + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-apollo-and-cache-persist-typescript&project-name=with-apollo-and-cache-persist-typescript&repository-name=with-apollo-and-cache-persist-typescript) + +## How to use + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: + +```bash +npx create-next-app --example with-apollo-and-cache-persist-typescript with-apollo-and-cache-persist-typescript-app +# or +yarn create next-app --example with-apollo-and-cache-persist-typescript with-apollo-and-cache-persist-typescript-app +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). diff --git a/examples/with-apollo-and-cache-persist-typescript/lib/apollo/apolloClient.ts b/examples/with-apollo-and-cache-persist-typescript/lib/apollo/apolloClient.ts new file mode 100644 index 000000000000..ac98ea9cc461 --- /dev/null +++ b/examples/with-apollo-and-cache-persist-typescript/lib/apollo/apolloClient.ts @@ -0,0 +1,174 @@ +import { useEffect, useState } from 'react' +import { GetServerSidePropsResult, GetStaticPropsResult } from 'next' +import { + ApolloCache, + ApolloClient, + HttpLink, + InMemoryCache, + NormalizedCacheObject, +} from '@apollo/client' +import merge from 'deepmerge' +import isDeepEqual from 'fast-deep-equal/react' +import { useEffectOnce, usePrevious } from 'react-use' +import { CachePersistor, LocalStorageWrapper } from 'apollo3-cache-persist' + +export const PERSISTOR_CACHE_KEY = + 'with-apollo-and-cache-persist-typescript__apollo-persisted-cache' + +const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__' + +const isServer = (): boolean => typeof window === 'undefined' + +let apolloClient: ApolloClient + +function createCache() { + return new InMemoryCache({ + typePolicies: { + Info: { + keyFields: ['name'], + }, + }, + }) +} + +function createApolloClient(cache?: ApolloCache) { + return new ApolloClient({ + ssrMode: isServer(), + link: new HttpLink({ + uri: 'https://api.spacex.land/graphql', // Your API URL here + credentials: 'same-origin', + }), + cache: cache || createCache(), + }) +} + +function mergeCache( + cache1: NormalizedCacheObject, + cache2: NormalizedCacheObject +) { + return merge(cache1, cache2, { + // Combine arrays using object equality (like in sets) + arrayMerge: (destinationArray, sourceArray) => [ + ...sourceArray, + ...destinationArray.filter((d) => + sourceArray.every((s) => !isDeepEqual(d, s)) + ), + ], + }) +} + +export function initializeApollo( + cache?: ApolloCache +): ApolloClient { + const _apolloClient = apolloClient ?? createApolloClient(cache) + + // For SSG and SSR always create a new Apollo Client + if (isServer()) return _apolloClient + + // Create the Apollo Client once in the client + if (!apolloClient) apolloClient = _apolloClient + + return _apolloClient +} + +export function addApolloState< + P extends + | GetServerSidePropsResult> + | GetStaticPropsResult> +>( + client: ApolloClient, + pageProps: P, + existingCache?: NormalizedCacheObject +): P { + if (pageProps && 'props' in pageProps) { + const props = pageProps.props + + if (existingCache) { + props[APOLLO_STATE_PROP_NAME] = mergeCache( + client.cache.extract(), + existingCache + ) + } else { + props[APOLLO_STATE_PROP_NAME] = client.cache.extract() + } + } + + return pageProps +} + +function mergeAndRestoreCache( + client: ApolloClient, + state: NormalizedCacheObject | undefined +) { + if (!state) return + + // Get existing cache, loaded during client side data fetching + const existingCache = client.extract() + // Merge the existing cache into data passed from getStaticProps/getServerSideProps + const data = mergeCache(state, existingCache) + // Restore the cache with the merged data + client.cache.restore(data) +} + +export function useApollo(pageProps: Record): { + client: ApolloClient | undefined + cachePersistor: CachePersistor | undefined +} { + const state = pageProps[APOLLO_STATE_PROP_NAME] as + | NormalizedCacheObject + | undefined + const previousState = usePrevious(state) + + const [client, setClient] = useState>() + const [cachePersistor, setCachePersistor] = + useState>() + + useEffectOnce(() => { + async function init() { + const cache = createCache() + + const cachePersistor = new CachePersistor({ + cache, + storage: new LocalStorageWrapper(window.localStorage), + debug: process.env.NODE_ENV === 'development', + key: PERSISTOR_CACHE_KEY, + }) + + // Restore client side persisted data before letting the application to + // run any queries + await cachePersistor.restore() + + const client = initializeApollo(cache) + + mergeAndRestoreCache(client, state) + + // Trigger persist to persist data from SSR + cachePersistor.persist() + + setCachePersistor(cachePersistor) + setClient(client) + } + + init() + }) + + useEffect(() => { + // If your page has Next.js data fetching methods that use Apollo Client, the initial state + // gets hydrated here during page transitions + if ( + client && + state && + previousState && + !isDeepEqual(state, previousState) + ) { + mergeAndRestoreCache(client, state) + + if (cachePersistor) { + // Trigger persist to persist data from SSR + cachePersistor.persist() + } + } + }, [state, previousState, client, cachePersistor]) + + return { client, cachePersistor } +} diff --git a/examples/with-apollo-and-cache-persist-typescript/next-env.d.ts b/examples/with-apollo-and-cache-persist-typescript/next-env.d.ts new file mode 100644 index 000000000000..9bc3dd46b9d9 --- /dev/null +++ b/examples/with-apollo-and-cache-persist-typescript/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/with-apollo-and-cache-persist-typescript/next.config.js b/examples/with-apollo-and-cache-persist-typescript/next.config.js new file mode 100644 index 000000000000..8b61df4e50f8 --- /dev/null +++ b/examples/with-apollo-and-cache-persist-typescript/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +module.exports = { + reactStrictMode: true, +} diff --git a/examples/with-apollo-and-cache-persist-typescript/package.json b/examples/with-apollo-and-cache-persist-typescript/package.json new file mode 100644 index 000000000000..1315136e767e --- /dev/null +++ b/examples/with-apollo-and-cache-persist-typescript/package.json @@ -0,0 +1,26 @@ +{ + "name": "with-apollo-and-cache-persist-typescript", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@apollo/client": "^3.4.16", + "apollo3-cache-persist": "^0.13.0", + "deepmerge": "^4.2.2", + "next": "11.1.2", + "react": "17.0.2", + "react-dom": "17.0.2", + "react-use": "^17.3.1" + }, + "devDependencies": { + "@types/react": "17.0.27", + "eslint": "7.32.0", + "eslint-config-next": "11.1.2", + "typescript": "4.4.3" + } +} diff --git a/examples/with-apollo-and-cache-persist-typescript/pages/_app.tsx b/examples/with-apollo-and-cache-persist-typescript/pages/_app.tsx new file mode 100644 index 000000000000..995addf61b02 --- /dev/null +++ b/examples/with-apollo-and-cache-persist-typescript/pages/_app.tsx @@ -0,0 +1,25 @@ +import 'styles/globals.css' + +import type { AppProps } from 'next/app' +import { ApolloProvider } from '@apollo/client' +import { useApollo } from 'lib/apollo/apolloClient' + +function MyApp({ Component, pageProps }: AppProps) { + const { client } = useApollo(pageProps) + + // We need to wait for the client side cache to be restored before rendering + // the application + if (!client) { + return
Initializing...
+ } + + return ( + +
+ +
+
+ ) +} + +export default MyApp diff --git a/examples/with-apollo-and-cache-persist-typescript/pages/index.tsx b/examples/with-apollo-and-cache-persist-typescript/pages/index.tsx new file mode 100644 index 000000000000..9bb08ca13b78 --- /dev/null +++ b/examples/with-apollo-and-cache-persist-typescript/pages/index.tsx @@ -0,0 +1,56 @@ +import type { NextPage, GetServerSideProps } from 'next' +import Link from 'next/link' +import { + addApolloState, + initializeApollo, + PERSISTOR_CACHE_KEY, +} from 'lib/apollo/apolloClient' +import { GET_COMPANY_DATA_QUERY } from 'queries/getCompanyData' +import styles from 'styles/Home.module.css' + +type Props = { + company: { name: string; summary: string } +} + +const Home: NextPage = ({ company }) => { + const { name, summary } = company + + return ( + <> +

Welcome to the {name} launches list!

+
{summary}
+
+ After first load, cache will be added to the local storage with a key + called {PERSISTOR_CACHE_KEY}. When you will go to the /list page, + cache will be populated with more data. Try to reload the application or + visit it in other tab and go the /list to see that data will be loaded + from the cache that was persisted in local storage. +
+
+ Go to the launches list! +
+ + ) +} + +export const getServerSideProps: GetServerSideProps = async () => { + try { + const apolloClient = initializeApollo() + + const { + data: { company }, + } = await apolloClient.query<{ + company: { name: string; summary: string } + }>({ + query: GET_COMPANY_DATA_QUERY, + }) + + return addApolloState(apolloClient, { props: { company } }) + } catch (error) { + console.log(error) + + return { notFound: true } + } +} + +export default Home diff --git a/examples/with-apollo-and-cache-persist-typescript/pages/list.tsx b/examples/with-apollo-and-cache-persist-typescript/pages/list.tsx new file mode 100644 index 000000000000..f81f7e1e1a14 --- /dev/null +++ b/examples/with-apollo-and-cache-persist-typescript/pages/list.tsx @@ -0,0 +1,34 @@ +import { useQuery } from '@apollo/client' +import type { NextPage } from 'next' +import { useRouter } from 'next/dist/client/router' +import { GET_LAUNCHES_LIST_QUERY } from 'queries/getLaunchesList' + +const List: NextPage = () => { + const { back } = useRouter() + const { data: { launches = [] } = {}, loading } = useQuery<{ + launches: { id: string; mission_id: string[]; mission_name: string }[] + }>(GET_LAUNCHES_LIST_QUERY) + + if (loading) { + return
Loading...
+ } + + return ( + <> + + {launches.length > 0 ? ( +
    + {Array.from(new Set(launches)).map( + ({ mission_name, mission_id, id }) => ( +
  • {mission_name}
  • + ) + )} +
+ ) : ( +
No data
+ )} + + ) +} + +export default List diff --git a/examples/with-apollo-and-cache-persist-typescript/public/favicon.ico b/examples/with-apollo-and-cache-persist-typescript/public/favicon.ico new file mode 100644 index 000000000000..718d6fea4835 Binary files /dev/null and b/examples/with-apollo-and-cache-persist-typescript/public/favicon.ico differ diff --git a/examples/with-apollo-and-cache-persist-typescript/public/vercel.svg b/examples/with-apollo-and-cache-persist-typescript/public/vercel.svg new file mode 100644 index 000000000000..fbf0e25a651c --- /dev/null +++ b/examples/with-apollo-and-cache-persist-typescript/public/vercel.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/examples/with-apollo-and-cache-persist-typescript/queries/getCompanyData.ts b/examples/with-apollo-and-cache-persist-typescript/queries/getCompanyData.ts new file mode 100644 index 000000000000..59a14808742a --- /dev/null +++ b/examples/with-apollo-and-cache-persist-typescript/queries/getCompanyData.ts @@ -0,0 +1,10 @@ +import gql from 'graphql-tag' + +export const GET_COMPANY_DATA_QUERY = gql` + query getCompanyData { + company { + name + summary + } + } +` diff --git a/examples/with-apollo-and-cache-persist-typescript/queries/getLaunchesList.ts b/examples/with-apollo-and-cache-persist-typescript/queries/getLaunchesList.ts new file mode 100644 index 000000000000..595b2931da58 --- /dev/null +++ b/examples/with-apollo-and-cache-persist-typescript/queries/getLaunchesList.ts @@ -0,0 +1,11 @@ +import gql from 'graphql-tag' + +export const GET_LAUNCHES_LIST_QUERY = gql` + query getLaunchesList { + launches { + id + mission_name + mission_id + } + } +` diff --git a/examples/with-apollo-and-cache-persist-typescript/styles/Home.module.css b/examples/with-apollo-and-cache-persist-typescript/styles/Home.module.css new file mode 100644 index 000000000000..4336ce7b9e58 --- /dev/null +++ b/examples/with-apollo-and-cache-persist-typescript/styles/Home.module.css @@ -0,0 +1,12 @@ +.wrapper { + padding: 15px; +} + +.link { + margin-top: 20px; + text-decoration: underline; +} + +.info { + margin-top: 20px; +} diff --git a/examples/with-apollo-and-cache-persist-typescript/styles/globals.css b/examples/with-apollo-and-cache-persist-typescript/styles/globals.css new file mode 100644 index 000000000000..e5e2dcc23baf --- /dev/null +++ b/examples/with-apollo-and-cache-persist-typescript/styles/globals.css @@ -0,0 +1,16 @@ +html, +body { + padding: 0; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, + Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; +} + +a { + color: inherit; + text-decoration: none; +} + +* { + box-sizing: border-box; +} diff --git a/examples/with-apollo-and-cache-persist-typescript/tsconfig.json b/examples/with-apollo-and-cache-persist-typescript/tsconfig.json new file mode 100644 index 000000000000..cf575c7eb51b --- /dev/null +++ b/examples/with-apollo-and-cache-persist-typescript/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +}