Add some more clarity to the RQ docs (#10120)
This commit is contained in:
@@ -431,16 +431,30 @@ yarn intl:compile # Compile translations for runtime
|
||||
// src/state/queries/profile.ts
|
||||
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
// Query key pattern
|
||||
const RQKEY_ROOT = 'profile'
|
||||
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
||||
import {createQueryKey} from '#/state/queries/util'
|
||||
|
||||
// Query hook
|
||||
/*
|
||||
* Query key name should match the query hook name for consistency
|
||||
*/
|
||||
const profileQueryKeyRoot = 'profile'
|
||||
|
||||
/*
|
||||
* Use object params and createQueryKey helper for better readability and to
|
||||
* avoid bugs with parameter order or types.
|
||||
*/
|
||||
export const createProfileQueryKey = (args: {did: string}) =>
|
||||
createQueryKey(profileQueryKeyRoot, args)
|
||||
|
||||
/*
|
||||
* Query hook should be named use[Name]Query, where [Name] describes the data
|
||||
* being fetched. This is not a strict requirement, but it's a helpful
|
||||
* convention for discoverability
|
||||
*/
|
||||
export function useProfileQuery({did}: {did: string}) {
|
||||
const agent = useAgent()
|
||||
|
||||
return useQuery({
|
||||
queryKey: RQKEY(did),
|
||||
queryKey: createProfileQueryKey({did}),
|
||||
queryFn: async () => {
|
||||
const res = await agent.getProfile({actor: did})
|
||||
return res.data
|
||||
@@ -450,8 +464,12 @@ export function useProfileQuery({did}: {did: string}) {
|
||||
})
|
||||
}
|
||||
|
||||
// Mutation hook
|
||||
export function useUpdateProfile() {
|
||||
/*
|
||||
* Mutation hook should match the name of the query hook, but with "Mutation"
|
||||
* suffix. This is not a strict requirement, but it's a helpful convention for
|
||||
* discoverability and consistency.
|
||||
*/
|
||||
export function useProfileMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
@@ -459,7 +477,9 @@ export function useUpdateProfile() {
|
||||
// Update logic
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({queryKey: RQKEY(variables.did)})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: createProfileQueryKey({did: variables.did}),
|
||||
})
|
||||
},
|
||||
onError: (error) => {
|
||||
if (isNetworkError(error)) {
|
||||
@@ -473,6 +493,24 @@ export function useUpdateProfile() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* If cache mutation is needed, include specific interfaces for the specific
|
||||
* mutations you require adjacent to the source queries. Naming should be
|
||||
* descriptive of the mutation's purpose, e.g. use[Name]CacheMutation. This is
|
||||
* not a strict requirement, but it's a helpful convention for discoverability
|
||||
* and consistency.
|
||||
*/
|
||||
export function useProfileCacheMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return (data: Partial<Profile>) => {
|
||||
queryClient.setQueryData(createProfileQueryKey({did: data.did}), oldData => {
|
||||
if (!oldData) return oldData
|
||||
return {...oldData, ...data}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Stale Time Constants** (from `src/state/queries/index.ts`):
|
||||
@@ -491,7 +529,7 @@ export function useDraftsQuery() {
|
||||
const agent = useAgent()
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: ['drafts'],
|
||||
queryKey: createQueryKey('drafts'),
|
||||
queryFn: async ({pageParam}) => {
|
||||
const res = await agent.app.bsky.draft.getDrafts({cursor: pageParam})
|
||||
return res.data
|
||||
@@ -504,6 +542,19 @@ export function useDraftsQuery() {
|
||||
|
||||
To get all items from pages: `data?.pages.flatMap(page => page.items) ?? []`
|
||||
|
||||
**Persisted Queries**
|
||||
|
||||
To persist query data across app restarts, `createQueryKey` supports a third
|
||||
parameter called `options`, which has a `persistedVersion` property. When this
|
||||
property is set to a number, the query will be persisted.
|
||||
|
||||
When this property is updated (e.g. incremented), the persisted data will be cleared and replaced with the new data from the query function. This is useful for cases where the shape of the data has changed and old persisted data would no longer be valid.
|
||||
|
||||
```tsx
|
||||
export const createProfileQueryKey = (args: {did: string}) =>
|
||||
createQueryKey(profileQueryKeyRoot, args, {persistedVersion: 1})
|
||||
```
|
||||
|
||||
### Preferences (React Context)
|
||||
|
||||
```tsx
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
|
||||
import {createPersistedQueryStorage} from '#/lib/persisted-query-storage'
|
||||
import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
|
||||
import {PERSISTED_QUERY_ROOT} from '#/state/queries'
|
||||
import {isQueryPersisted} from '#/state/queries/util'
|
||||
import * as env from '#/env'
|
||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||
|
||||
@@ -137,8 +137,7 @@ const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehyd
|
||||
{
|
||||
shouldDehydrateMutation: (_: any) => false,
|
||||
shouldDehydrateQuery: query => {
|
||||
const root = String(query.queryKey[0])
|
||||
return root === PERSISTED_QUERY_ROOT
|
||||
return isQueryPersisted(query.queryKey)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+18
-11
@@ -22,13 +22,10 @@ import {
|
||||
import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {
|
||||
PERSISTED_QUERY_GCTIME,
|
||||
PERSISTED_QUERY_ROOT,
|
||||
STALE,
|
||||
} from '#/state/queries'
|
||||
import {GCTIME, STALE} from '#/state/queries'
|
||||
import {RQKEY as listQueryKey} from '#/state/queries/list'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {createQueryKey} from '#/state/queries/util'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {router} from '#/routes'
|
||||
import {useModerationOpts} from '../preferences/moderation-opts'
|
||||
@@ -416,10 +413,20 @@ const PWI_DISCOVER_FEED_STUB: SavedFeedSourceInfo = {
|
||||
contentMode: undefined,
|
||||
}
|
||||
|
||||
const createPinnedFeedInfosQueryKeyRoot = (
|
||||
const createPinnedFeedInfosQueryKey = (
|
||||
kind: 'pinned' | 'saved',
|
||||
feedUris: string[],
|
||||
) => [PERSISTED_QUERY_ROOT, 'feed-info', kind, feedUris]
|
||||
) =>
|
||||
createQueryKey(
|
||||
'feed-info',
|
||||
{
|
||||
kind,
|
||||
feedUris,
|
||||
},
|
||||
{
|
||||
persistedVersion: 1,
|
||||
},
|
||||
)
|
||||
|
||||
export function usePinnedFeedsInfos() {
|
||||
const {hasSession} = useSession()
|
||||
@@ -428,11 +435,11 @@ export function usePinnedFeedsInfos() {
|
||||
const pinnedItems = preferences?.savedFeeds.filter(feed => feed.pinned) ?? []
|
||||
|
||||
return useQuery({
|
||||
queryKey: createPinnedFeedInfosQueryKeyRoot(
|
||||
queryKey: createPinnedFeedInfosQueryKey(
|
||||
'pinned',
|
||||
pinnedItems.map(f => f.value),
|
||||
),
|
||||
gcTime: PERSISTED_QUERY_GCTIME,
|
||||
gcTime: GCTIME.INFINITY,
|
||||
staleTime: STALE.INFINITY,
|
||||
enabled: !isLoadingPrefs,
|
||||
queryFn: async () => {
|
||||
@@ -536,11 +543,11 @@ export function useSavedFeeds() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useQuery({
|
||||
queryKey: createPinnedFeedInfosQueryKeyRoot(
|
||||
queryKey: createPinnedFeedInfosQueryKey(
|
||||
'saved',
|
||||
savedItems.map(f => f.value),
|
||||
),
|
||||
gcTime: PERSISTED_QUERY_GCTIME,
|
||||
gcTime: GCTIME.INFINITY,
|
||||
staleTime: STALE.INFINITY,
|
||||
enabled: !isLoadingPrefs,
|
||||
placeholderData: previousData => {
|
||||
|
||||
@@ -19,22 +19,6 @@ export const STALE = {
|
||||
INFINITY: Infinity,
|
||||
}
|
||||
|
||||
/**
|
||||
* Root key for persisted queries.
|
||||
*
|
||||
* If the `querykey` of your query uses this at index 0, it will be
|
||||
* persisted automatically by the `PersistQueryClientProvider` in
|
||||
* `#/lib/react-query.tsx`.
|
||||
*
|
||||
* Be careful when using this, since it will change the query key and may
|
||||
* break any cases where we call `invalidateQueries` or `refetchQueries`
|
||||
* with the old key.
|
||||
*
|
||||
* Also, only use this for queries that are safe to persist between
|
||||
* app launches (like user preferences).
|
||||
*
|
||||
* Note that for queries that are persisted, it is recommended to extend
|
||||
* the `gcTime` to a longer duration, otherwise it'll get busted
|
||||
*/
|
||||
export const PERSISTED_QUERY_ROOT = 'PERSISTED'
|
||||
export const PERSISTED_QUERY_GCTIME = Infinity
|
||||
export const GCTIME = {
|
||||
INFINITY: Infinity,
|
||||
}
|
||||
|
||||
@@ -3,15 +3,12 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
import {z} from 'zod'
|
||||
|
||||
import {MAX_LABELERS} from '#/lib/constants'
|
||||
import {
|
||||
PERSISTED_QUERY_GCTIME,
|
||||
PERSISTED_QUERY_ROOT,
|
||||
STALE,
|
||||
} from '#/state/queries'
|
||||
import {GCTIME, STALE} from '#/state/queries'
|
||||
import {
|
||||
preferencesQueryKey,
|
||||
usePreferencesQuery,
|
||||
} from '#/state/queries/preferences'
|
||||
import {createQueryKey} from '#/state/queries/util'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const labelerInfoQueryKeyRoot = 'labeler-info'
|
||||
@@ -26,11 +23,8 @@ export const labelersInfoQueryKey = (dids: string[]) => [
|
||||
dids.slice().sort(),
|
||||
]
|
||||
|
||||
const persistedLabelersDetailedInfoQueryKey = (dids: string[]) => [
|
||||
PERSISTED_QUERY_ROOT,
|
||||
'labelers-detailed-info',
|
||||
dids,
|
||||
]
|
||||
const createLabelersDetailedInfoQueryKey = (dids: string[]) =>
|
||||
createQueryKey('labelers-detailed-info', {dids}, {persistedVersion: 1})
|
||||
|
||||
export function useLabelerInfoQuery({
|
||||
did,
|
||||
@@ -69,8 +63,8 @@ export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
|
||||
const agent = useAgent()
|
||||
return useQuery({
|
||||
enabled: !!dids.length,
|
||||
queryKey: persistedLabelersDetailedInfoQueryKey(dids),
|
||||
gcTime: PERSISTED_QUERY_GCTIME,
|
||||
queryKey: createLabelersDetailedInfoQueryKey(dids),
|
||||
gcTime: GCTIME.INFINITY,
|
||||
staleTime: STALE.MINUTES.ONE,
|
||||
queryFn: async () => {
|
||||
const res = await agent.app.bsky.labeler.getServices({
|
||||
|
||||
@@ -9,11 +9,7 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
import {PROD_DEFAULT_FEED} from '#/lib/constants'
|
||||
import {replaceEqualDeep} from '#/lib/functions'
|
||||
import {getAge} from '#/lib/strings/time'
|
||||
import {
|
||||
PERSISTED_QUERY_GCTIME,
|
||||
PERSISTED_QUERY_ROOT,
|
||||
STALE,
|
||||
} from '#/state/queries'
|
||||
import {GCTIME, STALE} from '#/state/queries'
|
||||
import {
|
||||
DEFAULT_HOME_FEED_PREFS,
|
||||
DEFAULT_LOGGED_OUT_PREFERENCES,
|
||||
@@ -23,6 +19,7 @@ import {
|
||||
type ThreadViewPreferences,
|
||||
type UsePreferencesQueryResponse,
|
||||
} from '#/state/queries/preferences/types'
|
||||
import {createQueryKey} from '#/state/queries/util'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {saveLabelers} from '#/state/session/agent-config'
|
||||
import {useAgeAssurance} from '#/ageAssurance'
|
||||
@@ -33,7 +30,11 @@ export * from '#/state/queries/preferences/const'
|
||||
export * from '#/state/queries/preferences/moderation'
|
||||
export * from '#/state/queries/preferences/types'
|
||||
|
||||
export const preferencesQueryKey = [PERSISTED_QUERY_ROOT, 'getPreferences']
|
||||
export const preferencesQueryKey = createQueryKey(
|
||||
'getPreferences',
|
||||
{},
|
||||
{persistedVersion: 1},
|
||||
)
|
||||
|
||||
export function usePreferencesQuery() {
|
||||
const agent = useAgent()
|
||||
@@ -44,7 +45,7 @@ export function usePreferencesQuery() {
|
||||
structuralSharing: replaceEqualDeep,
|
||||
refetchOnWindowFocus: true,
|
||||
queryKey: preferencesQueryKey,
|
||||
gcTime: PERSISTED_QUERY_GCTIME,
|
||||
gcTime: GCTIME.INFINITY,
|
||||
queryFn: async () => {
|
||||
if (!agent.did) {
|
||||
return DEFAULT_LOGGED_OUT_PREFERENCES
|
||||
|
||||
@@ -14,6 +14,61 @@ import {
|
||||
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
export type StructuredQueryKey<T extends Record<string, unknown>> = readonly [
|
||||
string,
|
||||
T,
|
||||
{
|
||||
persistedVersion?: number
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Helper method to ensure consistent query keys and key ordering
|
||||
*/
|
||||
export function createQueryKey<T extends Record<string, unknown>>(
|
||||
/**
|
||||
* The query key root. All queries must have a root.
|
||||
*/
|
||||
root: string,
|
||||
/**
|
||||
* Any arguments the query depends on, and if changed, should result in the query being refetched.
|
||||
*/
|
||||
args: T,
|
||||
options: {
|
||||
/**
|
||||
* If provided, this indicates that the query is persisted and the version
|
||||
* of the persisted query format.
|
||||
*
|
||||
* This is used to ensure that when we make breaking changes to the
|
||||
* persisted query format, we can increment the version and avoid trying to
|
||||
* read old persisted queries with the new format.
|
||||
*
|
||||
* If you're persisting your queries, you probably want to set `gcTime:
|
||||
* GCTIME.INFINITY` for this query, otherwise it'll get busted immediately
|
||||
* after being persisted.
|
||||
*/
|
||||
persistedVersion?: number
|
||||
} = {},
|
||||
): StructuredQueryKey<T> {
|
||||
return [root, args, options] as const
|
||||
}
|
||||
|
||||
export function isQueryPersisted(
|
||||
queryKey: QueryKey,
|
||||
): queryKey is StructuredQueryKey<Record<string, unknown>> {
|
||||
return (
|
||||
Array.isArray(queryKey) &&
|
||||
queryKey.length === 3 &&
|
||||
typeof queryKey[0] === 'string' &&
|
||||
typeof queryKey[1] === 'object' &&
|
||||
queryKey[1] !== null &&
|
||||
typeof queryKey[2] === 'object' &&
|
||||
queryKey[2] !== null &&
|
||||
'persistedVersion' in queryKey[2] &&
|
||||
typeof queryKey[2].persistedVersion === 'number'
|
||||
)
|
||||
}
|
||||
|
||||
export async function truncateAndInvalidate<T = any>(
|
||||
queryClient: QueryClient,
|
||||
queryKey: QueryKey,
|
||||
|
||||
Reference in New Issue
Block a user