Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add some more clarity to the RQ docs (#10120)

authored by

Eric Bailey and committed by
GitHub
99257f28 011d8d2f

+152 -61
+60 -9
CLAUDE.md
··· 431 431 // src/state/queries/profile.ts 432 432 import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query' 433 433 434 - // Query key pattern 435 - const RQKEY_ROOT = 'profile' 436 - export const RQKEY = (did: string) => [RQKEY_ROOT, did] 434 + import {createQueryKey} from '#/state/queries/util' 435 + 436 + /* 437 + * Query key name should match the query hook name for consistency 438 + */ 439 + const profileQueryKeyRoot = 'profile' 437 440 438 - // Query hook 441 + /* 442 + * Use object params and createQueryKey helper for better readability and to 443 + * avoid bugs with parameter order or types. 444 + */ 445 + export const createProfileQueryKey = (args: {did: string}) => 446 + createQueryKey(profileQueryKeyRoot, args) 447 + 448 + /* 449 + * Query hook should be named use[Name]Query, where [Name] describes the data 450 + * being fetched. This is not a strict requirement, but it's a helpful 451 + * convention for discoverability 452 + */ 439 453 export function useProfileQuery({did}: {did: string}) { 440 454 const agent = useAgent() 441 455 442 456 return useQuery({ 443 - queryKey: RQKEY(did), 457 + queryKey: createProfileQueryKey({did}), 444 458 queryFn: async () => { 445 459 const res = await agent.getProfile({actor: did}) 446 460 return res.data ··· 450 464 }) 451 465 } 452 466 453 - // Mutation hook 454 - export function useUpdateProfile() { 467 + /* 468 + * Mutation hook should match the name of the query hook, but with "Mutation" 469 + * suffix. This is not a strict requirement, but it's a helpful convention for 470 + * discoverability and consistency. 471 + */ 472 + export function useProfileMutation() { 455 473 const queryClient = useQueryClient() 456 474 457 475 return useMutation({ ··· 459 477 // Update logic 460 478 }, 461 479 onSuccess: (_, variables) => { 462 - queryClient.invalidateQueries({queryKey: RQKEY(variables.did)}) 480 + queryClient.invalidateQueries({ 481 + queryKey: createProfileQueryKey({did: variables.did}), 482 + }) 463 483 }, 464 484 onError: (error) => { 465 485 if (isNetworkError(error)) { ··· 473 493 } 474 494 }) 475 495 } 496 + 497 + /* 498 + * If cache mutation is needed, include specific interfaces for the specific 499 + * mutations you require adjacent to the source queries. Naming should be 500 + * descriptive of the mutation's purpose, e.g. use[Name]CacheMutation. This is 501 + * not a strict requirement, but it's a helpful convention for discoverability 502 + * and consistency. 503 + */ 504 + export function useProfileCacheMutation() { 505 + const queryClient = useQueryClient() 506 + 507 + return (data: Partial<Profile>) => { 508 + queryClient.setQueryData(createProfileQueryKey({did: data.did}), oldData => { 509 + if (!oldData) return oldData 510 + return {...oldData, ...data} 511 + }) 512 + } 513 + } 476 514 ``` 477 515 478 516 **Stale Time Constants** (from `src/state/queries/index.ts`): ··· 491 529 const agent = useAgent() 492 530 493 531 return useInfiniteQuery({ 494 - queryKey: ['drafts'], 532 + queryKey: createQueryKey('drafts'), 495 533 queryFn: async ({pageParam}) => { 496 534 const res = await agent.app.bsky.draft.getDrafts({cursor: pageParam}) 497 535 return res.data ··· 503 541 ``` 504 542 505 543 To get all items from pages: `data?.pages.flatMap(page => page.items) ?? []` 544 + 545 + **Persisted Queries** 546 + 547 + To persist query data across app restarts, `createQueryKey` supports a third 548 + parameter called `options`, which has a `persistedVersion` property. When this 549 + property is set to a number, the query will be persisted. 550 + 551 + 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. 552 + 553 + ```tsx 554 + export const createProfileQueryKey = (args: {did: string}) => 555 + createQueryKey(profileQueryKeyRoot, args, {persistedVersion: 1}) 556 + ``` 506 557 507 558 ### Preferences (React Context) 508 559
+2 -3
src/lib/react-query.tsx
··· 10 10 11 11 import {createPersistedQueryStorage} from '#/lib/persisted-query-storage' 12 12 import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events' 13 - import {PERSISTED_QUERY_ROOT} from '#/state/queries' 13 + import {isQueryPersisted} from '#/state/queries/util' 14 14 import * as env from '#/env' 15 15 import {IS_NATIVE, IS_WEB} from '#/env' 16 16 ··· 137 137 { 138 138 shouldDehydrateMutation: (_: any) => false, 139 139 shouldDehydrateQuery: query => { 140 - const root = String(query.queryKey[0]) 141 - return root === PERSISTED_QUERY_ROOT 140 + return isQueryPersisted(query.queryKey) 142 141 }, 143 142 } 144 143
+18 -11
src/state/queries/feed.ts
··· 22 22 import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants' 23 23 import {sanitizeDisplayName} from '#/lib/strings/display-names' 24 24 import {sanitizeHandle} from '#/lib/strings/handles' 25 - import { 26 - PERSISTED_QUERY_GCTIME, 27 - PERSISTED_QUERY_ROOT, 28 - STALE, 29 - } from '#/state/queries' 25 + import {GCTIME, STALE} from '#/state/queries' 30 26 import {RQKEY as listQueryKey} from '#/state/queries/list' 31 27 import {usePreferencesQuery} from '#/state/queries/preferences' 28 + import {createQueryKey} from '#/state/queries/util' 32 29 import {useAgent, useSession} from '#/state/session' 33 30 import {router} from '#/routes' 34 31 import {useModerationOpts} from '../preferences/moderation-opts' ··· 416 413 contentMode: undefined, 417 414 } 418 415 419 - const createPinnedFeedInfosQueryKeyRoot = ( 416 + const createPinnedFeedInfosQueryKey = ( 420 417 kind: 'pinned' | 'saved', 421 418 feedUris: string[], 422 - ) => [PERSISTED_QUERY_ROOT, 'feed-info', kind, feedUris] 419 + ) => 420 + createQueryKey( 421 + 'feed-info', 422 + { 423 + kind, 424 + feedUris, 425 + }, 426 + { 427 + persistedVersion: 1, 428 + }, 429 + ) 423 430 424 431 export function usePinnedFeedsInfos() { 425 432 const {hasSession} = useSession() ··· 428 435 const pinnedItems = preferences?.savedFeeds.filter(feed => feed.pinned) ?? [] 429 436 430 437 return useQuery({ 431 - queryKey: createPinnedFeedInfosQueryKeyRoot( 438 + queryKey: createPinnedFeedInfosQueryKey( 432 439 'pinned', 433 440 pinnedItems.map(f => f.value), 434 441 ), 435 - gcTime: PERSISTED_QUERY_GCTIME, 442 + gcTime: GCTIME.INFINITY, 436 443 staleTime: STALE.INFINITY, 437 444 enabled: !isLoadingPrefs, 438 445 queryFn: async () => { ··· 536 543 const queryClient = useQueryClient() 537 544 538 545 return useQuery({ 539 - queryKey: createPinnedFeedInfosQueryKeyRoot( 546 + queryKey: createPinnedFeedInfosQueryKey( 540 547 'saved', 541 548 savedItems.map(f => f.value), 542 549 ), 543 - gcTime: PERSISTED_QUERY_GCTIME, 550 + gcTime: GCTIME.INFINITY, 544 551 staleTime: STALE.INFINITY, 545 552 enabled: !isLoadingPrefs, 546 553 placeholderData: previousData => {
+3 -19
src/state/queries/index.ts
··· 19 19 INFINITY: Infinity, 20 20 } 21 21 22 - /** 23 - * Root key for persisted queries. 24 - * 25 - * If the `querykey` of your query uses this at index 0, it will be 26 - * persisted automatically by the `PersistQueryClientProvider` in 27 - * `#/lib/react-query.tsx`. 28 - * 29 - * Be careful when using this, since it will change the query key and may 30 - * break any cases where we call `invalidateQueries` or `refetchQueries` 31 - * with the old key. 32 - * 33 - * Also, only use this for queries that are safe to persist between 34 - * app launches (like user preferences). 35 - * 36 - * Note that for queries that are persisted, it is recommended to extend 37 - * the `gcTime` to a longer duration, otherwise it'll get busted 38 - */ 39 - export const PERSISTED_QUERY_ROOT = 'PERSISTED' 40 - export const PERSISTED_QUERY_GCTIME = Infinity 22 + export const GCTIME = { 23 + INFINITY: Infinity, 24 + }
+6 -12
src/state/queries/labeler.ts
··· 3 3 import {z} from 'zod' 4 4 5 5 import {MAX_LABELERS} from '#/lib/constants' 6 - import { 7 - PERSISTED_QUERY_GCTIME, 8 - PERSISTED_QUERY_ROOT, 9 - STALE, 10 - } from '#/state/queries' 6 + import {GCTIME, STALE} from '#/state/queries' 11 7 import { 12 8 preferencesQueryKey, 13 9 usePreferencesQuery, 14 10 } from '#/state/queries/preferences' 11 + import {createQueryKey} from '#/state/queries/util' 15 12 import {useAgent} from '#/state/session' 16 13 17 14 const labelerInfoQueryKeyRoot = 'labeler-info' ··· 26 23 dids.slice().sort(), 27 24 ] 28 25 29 - const persistedLabelersDetailedInfoQueryKey = (dids: string[]) => [ 30 - PERSISTED_QUERY_ROOT, 31 - 'labelers-detailed-info', 32 - dids, 33 - ] 26 + const createLabelersDetailedInfoQueryKey = (dids: string[]) => 27 + createQueryKey('labelers-detailed-info', {dids}, {persistedVersion: 1}) 34 28 35 29 export function useLabelerInfoQuery({ 36 30 did, ··· 69 63 const agent = useAgent() 70 64 return useQuery({ 71 65 enabled: !!dids.length, 72 - queryKey: persistedLabelersDetailedInfoQueryKey(dids), 73 - gcTime: PERSISTED_QUERY_GCTIME, 66 + queryKey: createLabelersDetailedInfoQueryKey(dids), 67 + gcTime: GCTIME.INFINITY, 74 68 staleTime: STALE.MINUTES.ONE, 75 69 queryFn: async () => { 76 70 const res = await agent.app.bsky.labeler.getServices({
+8 -7
src/state/queries/preferences/index.ts
··· 9 9 import {PROD_DEFAULT_FEED} from '#/lib/constants' 10 10 import {replaceEqualDeep} from '#/lib/functions' 11 11 import {getAge} from '#/lib/strings/time' 12 - import { 13 - PERSISTED_QUERY_GCTIME, 14 - PERSISTED_QUERY_ROOT, 15 - STALE, 16 - } from '#/state/queries' 12 + import {GCTIME, STALE} from '#/state/queries' 17 13 import { 18 14 DEFAULT_HOME_FEED_PREFS, 19 15 DEFAULT_LOGGED_OUT_PREFERENCES, ··· 23 19 type ThreadViewPreferences, 24 20 type UsePreferencesQueryResponse, 25 21 } from '#/state/queries/preferences/types' 22 + import {createQueryKey} from '#/state/queries/util' 26 23 import {useAgent} from '#/state/session' 27 24 import {saveLabelers} from '#/state/session/agent-config' 28 25 import {useAgeAssurance} from '#/ageAssurance' ··· 33 30 export * from '#/state/queries/preferences/moderation' 34 31 export * from '#/state/queries/preferences/types' 35 32 36 - export const preferencesQueryKey = [PERSISTED_QUERY_ROOT, 'getPreferences'] 33 + export const preferencesQueryKey = createQueryKey( 34 + 'getPreferences', 35 + {}, 36 + {persistedVersion: 1}, 37 + ) 37 38 38 39 export function usePreferencesQuery() { 39 40 const agent = useAgent() ··· 44 45 structuralSharing: replaceEqualDeep, 45 46 refetchOnWindowFocus: true, 46 47 queryKey: preferencesQueryKey, 47 - gcTime: PERSISTED_QUERY_GCTIME, 48 + gcTime: GCTIME.INFINITY, 48 49 queryFn: async () => { 49 50 if (!agent.did) { 50 51 return DEFAULT_LOGGED_OUT_PREFERENCES
+55
src/state/queries/util.ts
··· 14 14 15 15 import * as bsky from '#/types/bsky' 16 16 17 + export type StructuredQueryKey<T extends Record<string, unknown>> = readonly [ 18 + string, 19 + T, 20 + { 21 + persistedVersion?: number 22 + }, 23 + ] 24 + 25 + /** 26 + * Helper method to ensure consistent query keys and key ordering 27 + */ 28 + export function createQueryKey<T extends Record<string, unknown>>( 29 + /** 30 + * The query key root. All queries must have a root. 31 + */ 32 + root: string, 33 + /** 34 + * Any arguments the query depends on, and if changed, should result in the query being refetched. 35 + */ 36 + args: T, 37 + options: { 38 + /** 39 + * If provided, this indicates that the query is persisted and the version 40 + * of the persisted query format. 41 + * 42 + * This is used to ensure that when we make breaking changes to the 43 + * persisted query format, we can increment the version and avoid trying to 44 + * read old persisted queries with the new format. 45 + * 46 + * If you're persisting your queries, you probably want to set `gcTime: 47 + * GCTIME.INFINITY` for this query, otherwise it'll get busted immediately 48 + * after being persisted. 49 + */ 50 + persistedVersion?: number 51 + } = {}, 52 + ): StructuredQueryKey<T> { 53 + return [root, args, options] as const 54 + } 55 + 56 + export function isQueryPersisted( 57 + queryKey: QueryKey, 58 + ): queryKey is StructuredQueryKey<Record<string, unknown>> { 59 + return ( 60 + Array.isArray(queryKey) && 61 + queryKey.length === 3 && 62 + typeof queryKey[0] === 'string' && 63 + typeof queryKey[1] === 'object' && 64 + queryKey[1] !== null && 65 + typeof queryKey[2] === 'object' && 66 + queryKey[2] !== null && 67 + 'persistedVersion' in queryKey[2] && 68 + typeof queryKey[2].persistedVersion === 'number' 69 + ) 70 + } 71 + 17 72 export async function truncateAndInvalidate<T = any>( 18 73 queryClient: QueryClient, 19 74 queryKey: QueryKey,