···431431// src/state/queries/profile.ts
432432import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
433433434434-// Query key pattern
435435-const RQKEY_ROOT = 'profile'
436436-export const RQKEY = (did: string) => [RQKEY_ROOT, did]
434434+import {createQueryKey} from '#/state/queries/util'
435435+436436+/*
437437+ * Query key name should match the query hook name for consistency
438438+ */
439439+const profileQueryKeyRoot = 'profile'
437440438438-// Query hook
441441+/*
442442+ * Use object params and createQueryKey helper for better readability and to
443443+ * avoid bugs with parameter order or types.
444444+ */
445445+export const createProfileQueryKey = (args: {did: string}) =>
446446+ createQueryKey(profileQueryKeyRoot, args)
447447+448448+/*
449449+ * Query hook should be named use[Name]Query, where [Name] describes the data
450450+ * being fetched. This is not a strict requirement, but it's a helpful
451451+ * convention for discoverability
452452+ */
439453export function useProfileQuery({did}: {did: string}) {
440454 const agent = useAgent()
441455442456 return useQuery({
443443- queryKey: RQKEY(did),
457457+ queryKey: createProfileQueryKey({did}),
444458 queryFn: async () => {
445459 const res = await agent.getProfile({actor: did})
446460 return res.data
···450464 })
451465}
452466453453-// Mutation hook
454454-export function useUpdateProfile() {
467467+/*
468468+ * Mutation hook should match the name of the query hook, but with "Mutation"
469469+ * suffix. This is not a strict requirement, but it's a helpful convention for
470470+ * discoverability and consistency.
471471+ */
472472+export function useProfileMutation() {
455473 const queryClient = useQueryClient()
456474457475 return useMutation({
···459477 // Update logic
460478 },
461479 onSuccess: (_, variables) => {
462462- queryClient.invalidateQueries({queryKey: RQKEY(variables.did)})
480480+ queryClient.invalidateQueries({
481481+ queryKey: createProfileQueryKey({did: variables.did}),
482482+ })
463483 },
464484 onError: (error) => {
465485 if (isNetworkError(error)) {
···473493 }
474494 })
475495}
496496+497497+/*
498498+ * If cache mutation is needed, include specific interfaces for the specific
499499+ * mutations you require adjacent to the source queries. Naming should be
500500+ * descriptive of the mutation's purpose, e.g. use[Name]CacheMutation. This is
501501+ * not a strict requirement, but it's a helpful convention for discoverability
502502+ * and consistency.
503503+ */
504504+export function useProfileCacheMutation() {
505505+ const queryClient = useQueryClient()
506506+507507+ return (data: Partial<Profile>) => {
508508+ queryClient.setQueryData(createProfileQueryKey({did: data.did}), oldData => {
509509+ if (!oldData) return oldData
510510+ return {...oldData, ...data}
511511+ })
512512+ }
513513+}
476514```
477515478516**Stale Time Constants** (from `src/state/queries/index.ts`):
···491529 const agent = useAgent()
492530493531 return useInfiniteQuery({
494494- queryKey: ['drafts'],
532532+ queryKey: createQueryKey('drafts'),
495533 queryFn: async ({pageParam}) => {
496534 const res = await agent.app.bsky.draft.getDrafts({cursor: pageParam})
497535 return res.data
···503541```
504542505543To get all items from pages: `data?.pages.flatMap(page => page.items) ?? []`
544544+545545+**Persisted Queries**
546546+547547+To persist query data across app restarts, `createQueryKey` supports a third
548548+parameter called `options`, which has a `persistedVersion` property. When this
549549+property is set to a number, the query will be persisted.
550550+551551+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.
552552+553553+```tsx
554554+export const createProfileQueryKey = (args: {did: string}) =>
555555+ createQueryKey(profileQueryKeyRoot, args, {persistedVersion: 1})
556556+```
506557507558### Preferences (React Context)
508559
+2-3
src/lib/react-query.tsx
···10101111import {createPersistedQueryStorage} from '#/lib/persisted-query-storage'
1212import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
1313-import {PERSISTED_QUERY_ROOT} from '#/state/queries'
1313+import {isQueryPersisted} from '#/state/queries/util'
1414import * as env from '#/env'
1515import {IS_NATIVE, IS_WEB} from '#/env'
1616···137137 {
138138 shouldDehydrateMutation: (_: any) => false,
139139 shouldDehydrateQuery: query => {
140140- const root = String(query.queryKey[0])
141141- return root === PERSISTED_QUERY_ROOT
140140+ return isQueryPersisted(query.queryKey)
142141 },
143142 }
144143
···1919 INFINITY: Infinity,
2020}
21212222-/**
2323- * Root key for persisted queries.
2424- *
2525- * If the `querykey` of your query uses this at index 0, it will be
2626- * persisted automatically by the `PersistQueryClientProvider` in
2727- * `#/lib/react-query.tsx`.
2828- *
2929- * Be careful when using this, since it will change the query key and may
3030- * break any cases where we call `invalidateQueries` or `refetchQueries`
3131- * with the old key.
3232- *
3333- * Also, only use this for queries that are safe to persist between
3434- * app launches (like user preferences).
3535- *
3636- * Note that for queries that are persisted, it is recommended to extend
3737- * the `gcTime` to a longer duration, otherwise it'll get busted
3838- */
3939-export const PERSISTED_QUERY_ROOT = 'PERSISTED'
4040-export const PERSISTED_QUERY_GCTIME = Infinity
2222+export const GCTIME = {
2323+ INFINITY: Infinity,
2424+}
···99import {PROD_DEFAULT_FEED} from '#/lib/constants'
1010import {replaceEqualDeep} from '#/lib/functions'
1111import {getAge} from '#/lib/strings/time'
1212-import {
1313- PERSISTED_QUERY_GCTIME,
1414- PERSISTED_QUERY_ROOT,
1515- STALE,
1616-} from '#/state/queries'
1212+import {GCTIME, STALE} from '#/state/queries'
1713import {
1814 DEFAULT_HOME_FEED_PREFS,
1915 DEFAULT_LOGGED_OUT_PREFERENCES,
···2319 type ThreadViewPreferences,
2420 type UsePreferencesQueryResponse,
2521} from '#/state/queries/preferences/types'
2222+import {createQueryKey} from '#/state/queries/util'
2623import {useAgent} from '#/state/session'
2724import {saveLabelers} from '#/state/session/agent-config'
2825import {useAgeAssurance} from '#/ageAssurance'
···3330export * from '#/state/queries/preferences/moderation'
3431export * from '#/state/queries/preferences/types'
35323636-export const preferencesQueryKey = [PERSISTED_QUERY_ROOT, 'getPreferences']
3333+export const preferencesQueryKey = createQueryKey(
3434+ 'getPreferences',
3535+ {},
3636+ {persistedVersion: 1},
3737+)
37383839export function usePreferencesQuery() {
3940 const agent = useAgent()
···4445 structuralSharing: replaceEqualDeep,
4546 refetchOnWindowFocus: true,
4647 queryKey: preferencesQueryKey,
4747- gcTime: PERSISTED_QUERY_GCTIME,
4848+ gcTime: GCTIME.INFINITY,
4849 queryFn: async () => {
4950 if (!agent.did) {
5051 return DEFAULT_LOGGED_OUT_PREFERENCES
+55
src/state/queries/util.ts
···14141515import * as bsky from '#/types/bsky'
16161717+export type StructuredQueryKey<T extends Record<string, unknown>> = readonly [
1818+ string,
1919+ T,
2020+ {
2121+ persistedVersion?: number
2222+ },
2323+]
2424+2525+/**
2626+ * Helper method to ensure consistent query keys and key ordering
2727+ */
2828+export function createQueryKey<T extends Record<string, unknown>>(
2929+ /**
3030+ * The query key root. All queries must have a root.
3131+ */
3232+ root: string,
3333+ /**
3434+ * Any arguments the query depends on, and if changed, should result in the query being refetched.
3535+ */
3636+ args: T,
3737+ options: {
3838+ /**
3939+ * If provided, this indicates that the query is persisted and the version
4040+ * of the persisted query format.
4141+ *
4242+ * This is used to ensure that when we make breaking changes to the
4343+ * persisted query format, we can increment the version and avoid trying to
4444+ * read old persisted queries with the new format.
4545+ *
4646+ * If you're persisting your queries, you probably want to set `gcTime:
4747+ * GCTIME.INFINITY` for this query, otherwise it'll get busted immediately
4848+ * after being persisted.
4949+ */
5050+ persistedVersion?: number
5151+ } = {},
5252+): StructuredQueryKey<T> {
5353+ return [root, args, options] as const
5454+}
5555+5656+export function isQueryPersisted(
5757+ queryKey: QueryKey,
5858+): queryKey is StructuredQueryKey<Record<string, unknown>> {
5959+ return (
6060+ Array.isArray(queryKey) &&
6161+ queryKey.length === 3 &&
6262+ typeof queryKey[0] === 'string' &&
6363+ typeof queryKey[1] === 'object' &&
6464+ queryKey[1] !== null &&
6565+ typeof queryKey[2] === 'object' &&
6666+ queryKey[2] !== null &&
6767+ 'persistedVersion' in queryKey[2] &&
6868+ typeof queryKey[2].persistedVersion === 'number'
6969+ )
7070+}
7171+1772export async function truncateAndInvalidate<T = any>(
1873 queryClient: QueryClient,
1974 queryKey: QueryKey,