@open-condo/apollo
v1.1.2
Published
A wrapper over @apollo/client that allows you to use persistent cache from local storage, configure TTL, invalidate cache, and use a single configuration for getServerSideProps, SSR, and CSR
Downloads
129
Maintainers
Readme
@open-condo/apollo
A wrapper over
@apollo/client
that allows you to use persistent cache from local storage, configure TTL, invalidate cache, and use a single configuration for getServerSideProps, SSR, and CSR.
Table of contents
Installation
Peer dependencies
NOTE: This package uses
react
/react-dom
and@apollo/client
as its peer dependencies, so make sure you've got ones installed.You should have no trouble with any react version having a hooks, but we're testing on versions
>=16
.Any apollo
3.x.x
should be fine too, but all utils are tested on^3.11.8
Installing packages
Install all (NPM)
npm i @open-condo/apollo react react-dom @apollo/client
Install all (Yarn)
yarn add @open-condo/apollo react react-dom @apollo/client
Usage
Basic setup
Init utils
To start using @open-condo/apollo
in your application, you must first configure ApolloHelper
and generate the necessary utilities. To do this, paste the following code somewhere in your application:
// ./lib/apollo.ts
import { ApolloHelper } from '@open-condo/apollo'
import type { InitCacheConfig, InitializeApollo, UseApollo } from '@open-condo/apollo'
import type { NormalizedCacheObject, ApolloClient } from '@apollo/client'
const serverUrl = process.env.SERVER_URL || 'http://localhost:3000'
const cacheConfig: InitCacheConfig = () => {
return {
invalidationPolicies: {
timeToLive: 15 * 60 * 1000, // 15 minutes in milliseconds
},
}
}
const apolloHelper = new ApolloHelper({
uri: `${serverUrl}/api/graphql`,
cacheConfig,
})
export const initializeApollo: InitializeApollo<ApolloClient<NormalizedCacheObject>> = apolloHelper.initializeApollo
export const useApollo: UseApollo<ApolloClient<NormalizedCacheObject>> = apolloHelper.generateUseApolloHook()
Init apollo client in your pages/_app.tsx
:
Then, simply use generated useApollo
hook to obtain client
and cachePersistor
,
which you can pass to your apps child components via standard ApolloProvider
:
import { ApolloProvider } from '@apollo/client'
import { CachePersistorContext } from '@open-condo/apollo'
import type { AppProps } from 'next/app'
import type { ReactNode } from 'react'
import { useApollo } from '@/lib/apollo'
export default function App ({ Component, pageProps, router }: AppProps): ReactNode {
const { client, cachePersistor } = useApollo(pageProps)
return (
<ApolloProvider client={client}>
<CachePersistorContext.Provider value={{ persistor: cachePersistor }}>
<Component {...pageProps} />
</CachePersistorContext.Provider>
</ApolloProvider>
)
}
After that, you can use any Apollo functions / hooks / utilities as you did before! 🥳
Client usage
Nothing additional is required to use Apollo in client components.
cachePersistor can be obtained from the provided useCachePersistor
hook
to avoid requests while the cache is being loaded.
import React from 'react'
import { useQuery } from '@apollo/client'
import { useCachePersistor } from '@open-condo/apollo'
const MyComponent: React.FC = () => {
const { persistor } = useCachePersistor()
const { data, loading } = useQuery({
query: ...,
variables: {},
skip: !persistor,
})
// ...
}
SSR usage
To use apollo in SSR environment, use generated initializeApollo
to obtain fresh client
and extractApolloState
to pass prefetched data to the client:
import React from 'react'
import { extractApolloState } from '@open-condo/apollo'
import { prepareSSRContext } from '@open-condo/miniapp-utils/helpers/apollo'
import { initializeApollo } from '@/lib/apollo'
import type { GetServerSideProps } from 'next'
const MyPage: React.FC = () => {
return null
}
export default MyPage
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
// NOTE: You should implement this function yourself depending on your business logic,
// Common pattern is to extract cookies to "cookie" header, or create Authorization header and so on
const { headers } = prepareSSRContext(req, res)
// Init new apollo with initial headers, which will be sent with each request
const client = initializeApollo({ headers })
await client.query({ ... })
// Extract fetched data to pageProps
return extractApolloState(client, {
props: { ... }
})
}
List pagination helpers
@open-condo/apollo
also provides a set of utilities to make it easier for you to work with list pagination.
To use them, initialise the ListHelper
class in your cacheConfig
like so:
// ./lib/apollo.ts
import { ApolloHelper } from '@open-condo/apollo'
import type { InitCacheConfig, InitializeApollo, UseApollo } from '@open-condo/apollo'
import type { NormalizedCacheObject, ApolloClient } from '@apollo/client'
const serverUrl = process.env.SERVER_URL || 'http://localhost:3000'
const cacheConfig: InitCacheConfig = (cacheOptions) => {
// Default helper, use skip / first as pagination arguments
const listHelper = new ListHelper({ cacheOptions })
// You can override pagination args like so
const customListHelper = new ListHelper({ cacheOptions, skipArgName: 'offset', firstArgName: 'limit' })
return {
typePolicies: {
Query: {
fields: {
allMeters: {
keyArgs: ['where'],
merge: listHelper.mergeLists,
read: listHelper.getReadFunction('paginate'),
},
allResidents: {
keyArgs: ['where'],
merge: listHelper.mergeLists,
read: listHelper.getReadFunction('showAll'),
},
customQuery: {
keyArgs: ['where'],
merge: customListHelper.mergeLists,
read: customListHelper.getReadFunction('paginate'),
}
},
},
},
invalidationPolicies: {
timeToLive: 15 * 60 * 1000, // 15 minutes in milliseconds
},
}
}
const apolloHelper = new ApolloHelper({
uri: `${serverUrl}/api/graphql`,
cacheConfig,
})
export const initializeApollo: InitializeApollo<ApolloClient<NormalizedCacheObject>> = apolloHelper.initializeApollo
export const useApollo: UseApollo<ApolloClient<NormalizedCacheObject>> = apolloHelper.generateUseApolloHook()
Dynamic API uri
ApolloHelper
can accept a function as uri
. This function is called when the client is initialised
(via initializeApollo or useApollo)
// ./lib/apollo.ts
import getConfig from 'next/config'
import { ApolloHelper } from '@open-condo/apollo'
import type { InitializeApollo, UseApollo } from '@open-condo/apollo'
import type { NormalizedCacheObject, ApolloClient } from '@apollo/client'
const { publicRuntimeConfig: { serviceUrl } } = getConfig()
/**
* Gets API url.
* If it's in SSR / production the absolute url is used
* In dev mode relative url is allowed on a client,
* so you can debug app on another device sharing the same network
*/
function getApiUrl () {
if (isDebug() && !isSSR()) {
return '/api/graphql'
}
return `${serviceUrl}/api/graphql`
}
const apolloHelper = new ApolloHelper({
uri: getApiUrl,
})
export const initializeApollo: InitializeApollo<ApolloClient<NormalizedCacheObject>> = apolloHelper.initializeApollo
export const useApollo: UseApollo<ApolloClient<NormalizedCacheObject>> = apolloHelper.generateUseApolloHook()
Middlewares
ApolloHelper
can accept a set of middlewares representing an ApolloLink | RequestHandler
type from @apollo/client
,
from which a common link is subsequently assembled using the from
utility from @apollo/client
.
This can be useful if your logic requires additional processing of all requests (headers / error handling, etc. etc.). You can see more details here
// ./lib/apollo.ts
import getConfig from 'next/config'
import { ApolloHelper } from '@open-condo/apollo'
import type { InitializeApollo, UseApollo } from '@open-condo/apollo'
import type { NormalizedCacheObject, ApolloClient } from '@apollo/client'
const { publicRuntimeConfig: { serviceUrl, revision } } = getConfig()
const apolloHelper = new ApolloHelper({
uri: `${serviceUrl}/api/graphql`,
middlewares: [
getTracingMiddleware({
serviceUrl,
codeVersion: revision,
}),
],
})
export const initializeApollo: InitializeApollo<ApolloClient<NormalizedCacheObject>> = apolloHelper.initializeApollo
export const useApollo: UseApollo<ApolloClient<NormalizedCacheObject>> = apolloHelper.generateUseApolloHook()
Cache invalidation
Cache from @open-condo/apollo
are extended from
@nerdwallet/apollo-cache-policies,
so you can freely explore and use their TTL mechanism.
const cacheConfig: InitCacheConfig = (cacheOptions) => {
const listHelper = new ListHelper({ cacheOptions })
return {
typePolicies: {
Query: {
fields: {
allMeters: {
keyArgs: ['where'],
merge: listHelper.mergeLists,
read: listHelper.getReadFunction('paginate'),
},
allResidents: {
keyArgs: ['where'],
merge: listHelper.mergeLists,
read: listHelper.getReadFunction('showAll'),
},
allServiceConsumers: {
keyArgs: ['where'],
merge: listHelper.mergeLists,
read: listHelper.getReadFunction('showAll'),
},
},
},
},
invalidationPolicies: {
timeToLive: 15 * 60 * 1000, // 15 minutes in milliseconds,
types: {
Contact: {
timeToLive: 2 * 60 * 60 * 1000, // 2 hours in milliseconds,
},
},
},
}
}
Cache identity
@open-condo/apollo
also provides a cache identification mechanism.
It allows not loading cache from localStorage
if its identity does not match the current clients cache
(obtained from SSR / CSR). By default, all caches are compared at the following path:
const DEFAULT_IDENTITY_PATH = ['ROOT_QUERY', 'authenticatedUser', '__ref']
To override it - pass the cacheIdentityKey
parameter to the cache configuration:
const cacheConfig: InitCacheConfig = (cacheOptions) => {
const listHelper = new ListHelper({ cacheOptions })
return {
cacheIdentityKey: ['ROOT_QUERY', 'me', 'id'],
}
}