@open-condo/apollo
Version:
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
366 lines (286 loc) • 12.1 kB
Markdown
[npm-badge-link]: https://img.shields.io/npm/v/@open-condo/apollo?style=flat-square
[npm-pkg-link]: https://www.npmjs.com/package/@open-condo/apollo
# `@open-condo/apollo` [![NPM][npm-badge-link]][npm-pkg-link]
> 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](#installation)
- [Peer dependencies](#peer-dependencies)
- [Installing packages](#installing-packages)
- [NPM](#install-all-npm)
- [Yarn](#install-all-yarn)
- [Usage](#usage)
- [Basic setup](#basic-setup)
1. [Init utils](#init-utils)
2. [Init apollo client in your _app.tsx](#init-apollo-client-in-your-pages_apptsx)
3. [Client usage](#client-usage)
4. [SSR usage](#ssr-usage)
- [List pagination helpers](#list-pagination-helpers)
- [Dynamic API uri](#dynamic-api-uri)
- [Middlewares](#middlewares)
- [Cache invalidation](#cache-invalidation)
- [Cache identity](#cache-identity)
## 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)
```bash
npm i @open-condo/apollo react react-dom @apollo/client
```
#### Install all (Yarn)
```bash
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:
```typescript
// ./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`:
```typescript jsx
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.
```typescript jsx
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:
```typescript
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:
```typescript
// ./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)
```typescript
// ./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](https://www.apollographql.com/docs/react/networking/advanced-http-networking/#customizing-request-logic)
```typescript
// ./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](https://github.com/NerdWalletOSS/apollo-cache-policies),
so you can freely explore and use their TTL mechanism.
```typescript
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:
```typescript
const DEFAULT_IDENTITY_PATH = ['ROOT_QUERY', 'authenticatedUser', '__ref']
```
To override it - pass the `cacheIdentityKey` parameter to the cache configuration:
```typescript
const cacheConfig: InitCacheConfig = (cacheOptions) => {
const listHelper = new ListHelper({ cacheOptions })
return {
cacheIdentityKey: ['ROOT_QUERY', 'me', 'id'],
}
}
```