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

With apollo and cache persist typescript #29718

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 34 additions & 0 deletions 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
27 changes: 27 additions & 0 deletions 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)).
@@ -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<NormalizedCacheObject>

function createCache() {
return new InMemoryCache({
typePolicies: {
Info: {
keyFields: ['name'],
},
},
})
}

function createApolloClient(cache?: ApolloCache<NormalizedCacheObject>) {
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<NormalizedCacheObject>
): ApolloClient<NormalizedCacheObject> {
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<Record<string, unknown>>
| GetStaticPropsResult<Record<string, unknown>>
>(
client: ApolloClient<NormalizedCacheObject>,
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<NormalizedCacheObject>,
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<string, unknown>): {
client: ApolloClient<NormalizedCacheObject> | undefined
cachePersistor: CachePersistor<NormalizedCacheObject> | undefined
} {
const state = pageProps[APOLLO_STATE_PROP_NAME] as
| NormalizedCacheObject
| undefined
const previousState = usePrevious(state)

const [client, setClient] = useState<ApolloClient<NormalizedCacheObject>>()
const [cachePersistor, setCachePersistor] =
useState<CachePersistor<NormalizedCacheObject>>()

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 }
}
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
module.exports = {
reactStrictMode: true,
}
26 changes: 26 additions & 0 deletions 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"
}
}
@@ -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 <div>Initializing...</div>
}

return (
<ApolloProvider client={client}>
<div style={{ padding: 15 }}>
<Component {...pageProps} />
</div>
</ApolloProvider>
)
}

export default MyApp
@@ -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<Props> = ({ company }) => {
const { name, summary } = company

return (
<>
<h1>Welcome to the {name} launches list!</h1>
<div>{summary}</div>
<div className={styles.info}>
After first load, cache will be added to the local storage with a key
called <b>{PERSISTOR_CACHE_KEY}</b>. 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.
</div>
<div className={styles.link}>
<Link href="/list">Go to the launches list!</Link>
</div>
</>
)
}

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
@@ -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 <div>Loading...</div>
}

return (
<>
<button onClick={back}>Back</button>
{launches.length > 0 ? (
<ul>
{Array.from(new Set(launches)).map(
({ mission_name, mission_id, id }) => (
<li key={`${id}-${mission_id[0]}`}>{mission_name}</li>
)
)}
</ul>
) : (
<div>No data</div>
)}
</>
)
}

export default List
Binary file not shown.