1.1.0 • Published 10 months ago

@open-condo/apollo v1.1.0

Weekly downloads
-
License
MIT
Repository
github
Last release
10 months ago

@open-condo/apollo NPM

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'],
    }
}