bsky-app/src/lib/react-query.tsx
dan b783745b2e
Extract query key roots to constants (#3404)
* Extract query key roots to constants

* Dedupe labelers-detailed-info

* Align naming
2024-04-04 18:57:38 +01:00

124 lines
4.1 KiB
TypeScript

import React, {useRef, useState} from 'react'
import {AppState, AppStateStatus} from 'react-native'
import AsyncStorage from '@react-native-async-storage/async-storage'
import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister'
import {focusManager, QueryClient} from '@tanstack/react-query'
import {
PersistQueryClientProvider,
PersistQueryClientProviderProps,
} from '@tanstack/react-query-persist-client'
import {isNative} from '#/platform/detection'
// any query keys in this array will be persisted to AsyncStorage
export const labelersDetailedInfoQueryKeyRoot = 'labelers-detailed-info'
const STORED_CACHE_QUERY_KEY_ROOTS = [labelersDetailedInfoQueryKeyRoot]
focusManager.setEventListener(onFocus => {
if (isNative) {
const subscription = AppState.addEventListener(
'change',
(status: AppStateStatus) => {
focusManager.setFocused(status === 'active')
},
)
return () => subscription.remove()
} else if (typeof window !== 'undefined' && window.addEventListener) {
// these handlers are a bit redundant but focus catches when the browser window
// is blurred/focused while visibilitychange seems to only handle when the
// window minimizes (both of them catch tab changes)
// there's no harm to redundant fires because refetchOnWindowFocus is only
// used with queries that employ stale data times
const handler = () => onFocus()
window.addEventListener('focus', handler, false)
window.addEventListener('visibilitychange', handler, false)
return () => {
window.removeEventListener('visibilitychange', handler)
window.removeEventListener('focus', handler)
}
}
})
const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
// NOTE
// refetchOnWindowFocus breaks some UIs (like feeds)
// so we only selectively want to enable this
// -prf
refetchOnWindowFocus: false,
// Structural sharing between responses makes it impossible to rely on
// "first seen" timestamps on objects to determine if they're fresh.
// Disable this optimization so that we can rely on "first seen" timestamps.
structuralSharing: false,
// We don't want to retry queries by default, because in most cases we
// want to fail early and show a response to the user. There are
// exceptions, and those can be made on a per-query basis. For others, we
// should give users controls to retry.
retry: false,
},
},
})
const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehydrateOptions'] =
{
shouldDehydrateMutation: (_: any) => false,
shouldDehydrateQuery: query => {
return STORED_CACHE_QUERY_KEY_ROOTS.includes(String(query.queryKey[0]))
},
}
export function QueryProvider({
children,
currentDid,
}: {
children: React.ReactNode
currentDid: string | undefined
}) {
return (
<QueryProviderInner
// Enforce we never reuse cache between users.
// These two props MUST stay in sync.
key={currentDid}
currentDid={currentDid}>
{children}
</QueryProviderInner>
)
}
function QueryProviderInner({
children,
currentDid,
}: {
children: React.ReactNode
currentDid: string | undefined
}) {
const initialDid = useRef(currentDid)
if (currentDid !== initialDid.current) {
throw Error(
'Something is very wrong. Expected did to be stable due to key above.',
)
}
// We create the query client here so that it's scoped to a specific DID.
// Do not move the query client creation outside of this component.
const [queryClient, _setQueryClient] = useState(() => createQueryClient())
const [persistOptions, _setPersistOptions] = useState(() => {
const asyncPersister = createAsyncStoragePersister({
storage: AsyncStorage,
key: 'queryClient-' + (currentDid ?? 'logged-out'),
})
return {
persister: asyncPersister,
dehydrateOptions,
}
})
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={persistOptions}>
{children}
</PersistQueryClientProvider>
)
}