Last active
July 14, 2021 21:03
-
-
Save jclem/cbfa9ac5b87a53d34b3f88438cf8054e to your computer and use it in GitHub Desktop.
A Next.js getInitialProps wrapper
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import localForage from 'localforage' | |
import {NextPage, NextPageContext} from 'next' | |
import {getDisplayName} from 'next/dist/next-server/lib/utils' | |
import {ComponentType} from 'react' | |
import { | |
hashQueryKey, | |
QueryFunction, | |
QueryKey, | |
useQuery, | |
UseQueryResult | |
} from 'react-query' | |
import {initQueryClient} from './api' | |
/** | |
* Props used internally by {@link withQuery} to wrap a {@link NextPage} and | |
* return one that accepts initial data and a {@link QueryKey} as props. | |
*/ | |
interface WithQueryWrapperProps<R, K extends QueryKey = QueryKey> { | |
queryKey: K | |
initialData: R | |
} | |
/** | |
* Props accepted by a component being wrapped by {@link withQuery}. | |
*/ | |
export interface WithQueryProps<R, K extends QueryKey = QueryKey> { | |
queryKey: K | |
result: UseQueryResult<R> | |
} | |
/** | |
* Options used to define how {@link withQuery} operates on a given component | |
* and its query. | |
*/ | |
export interface WithQueryOpts<K extends QueryKey = QueryKey> { | |
/** | |
* A function that receives a {@link NextPageContext} and returns the | |
* {@link QueryKey} for the wrapped page. | |
*/ | |
getQueryKey: (ctx: NextPageContext) => K | |
} | |
/** | |
* Wrap a {@link NextPage} component with a query for data with caching. | |
* | |
* Pages using this wrapper, when requested on fresh app load (a server request, | |
* not in-app navigation), will make an un-cached request for their data. When | |
* navigated to via in-app navigation, they'll check the in-memory cache, then | |
* an IndexedDB cache, and make a network request only if no cache already | |
* exists. | |
* | |
* Once the page renders, `withQuery` wraps a {@link useQuery} call that will | |
* then update the cache (and potentially re-render) in the background. | |
* | |
* Example: | |
* | |
* const MyPage = withQuery<MyQueryResponseData, string>( | |
* ({result}) => ( | |
* <div>Query Result: {result.data}</div> | |
* ), | |
* | |
* async ({queryKey}) => { | |
* const response = await fetch(`/api/some/data/${queryKey}`) | |
* return await response.json() | |
* }, | |
* | |
* { | |
* getQueryKey: (ctx) => ctx.query.id | |
* } | |
* ) | |
* | |
* Note that `withQuery` takes two type parameters: | |
* | |
* 1. `R` is the type of the data when the query succeeds. | |
* 2. `K` is the type of the query key. | |
* | |
* @param Component The React component to render, which receives the query key and query result as props | |
* @param query A function that queries for our data | |
* @param options An optional {@link WithQueryOpts} object | |
* @returns A NextPage component | |
*/ | |
export const withQuery = <R, K extends QueryKey = QueryKey>( | |
Component: ComponentType<WithQueryProps<R, K>>, | |
query: QueryFunction<R, K>, | |
opts: WithQueryOpts<K> | |
): NextPage<WithQueryWrapperProps<R, K>> => { | |
const {getQueryKey} = opts | |
// NOTE: Given we cast the default getter as `K`, we rely on the user to | |
// ensure their query key matches the pathname and query. | |
const queryWithLocalCache: QueryFunction<R, K> = async opts => { | |
const result = await query(opts) | |
if (result && typeof window !== 'undefined') { | |
const keyHash = hashQueryKey(opts.queryKey) | |
await localForage.setItem<R>(keyHash, result) | |
} | |
return result | |
} | |
// WrappedComponet is the component we return. It receives our initial data | |
// from `getInitialProps` and feeds it to `useQuery`, the result of which is | |
// passed as a prop to our user-provided component. | |
const WrappedComponent: NextPage<WithQueryWrapperProps<R, K>> = ({ | |
queryKey, | |
initialData | |
}) => { | |
const result = useQuery<R, unknown, R, K>(queryKey, queryWithLocalCache, { | |
initialData | |
}) | |
return <Component queryKey={queryKey} result={result} /> | |
} | |
// On the server, make an un-cached request for our data. On the client, check | |
// in-memory cache, then IndexedDB cache, then make an HTTP request if | |
// required. | |
WrappedComponent.getInitialProps = async ctx => { | |
const queryKey = getQueryKey(ctx) | |
const keyHash = hashQueryKey(queryKey) | |
const queryClient = initQueryClient() | |
const memoryCached = queryClient.getQueryData<R>(queryKey) | |
let initialData: R | null = null | |
if (memoryCached) { | |
initialData = memoryCached | |
} else if (typeof window !== 'undefined') { | |
const localCached = await localForage.getItem<R>(keyHash) | |
if (localCached) { | |
initialData = localCached | |
} | |
} | |
if (!initialData) { | |
initialData = await queryClient.fetchQuery<R, unknown, R, K>( | |
queryKey, | |
queryWithLocalCache | |
) | |
} | |
return { | |
queryKey, | |
initialData | |
} | |
} | |
WrappedComponent.displayName = `withQuery(${getDisplayName(Component)})` | |
return WrappedComponent | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment