diff --git a/CLAUDE.md b/CLAUDE.md index 5192660ea4..d7be980124 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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) => { + 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 diff --git a/src/state/queries/util.ts b/src/state/queries/util.ts index 363b6b6e02..839ccfb7b9 100644 --- a/src/state/queries/util.ts +++ b/src/state/queries/util.ts @@ -14,6 +14,30 @@ import { import * as bsky from '#/types/bsky' +/** + * Helper method to ensure consistent query keys and key ordering + */ +export function createQueryKey(root: string): readonly [string] +export function createQueryKey>( + root: string, + args: T, +): readonly [string, T] +export function createQueryKey>( + /** + * 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, +) { + if (args === undefined) { + return [root] as const + } + return [root, args] as const +} + export async function truncateAndInvalidate( queryClient: QueryClient, queryKey: QueryKey,