--- name: add-query description: Use when reading server data in the mobile app - wiring an @ecency/sdk query option into a screen, adding or editing a hook under src/providers/queries/, paginating a feed or list with an infinite query, or fixing a query key, enabled guard, or cache persistence problem. argument-hint: [query-name] --- # Add Query Read `CLAUDE.md` first (State Management, SDK Migration). Writes are a separate skill: `add-mutation`. ## Rule: the SDK owns the fetch `@ecency/sdk` exports a large family of `get*QueryOptions` helpers. Most files under `src/providers/queries/` import from `@ecency/sdk`; a bare `queryFn:` is rare in that directory. Search the SDK for your own domain first. Write a `queryFn` only when that search comes back empty: ```bash D=node_modules/@ecency/sdk/dist/browser/index.d.ts test -f "$D" || echo 'SDK not installed, run yarn' # a missing file greps as empty, a false all-clear grep -o "get[A-Za-z]*QueryOptions" "$D" | sort -u | grep -i draft # swap in your domain grep -n "declare function getPostQueryOptions" "$D" ``` Drop the trailing `| grep -i draft` to list them all. The last grep gives the real argument order. Do not guess it. In the call sites here, `getPostQueryOptions` takes the observer third. ## 1. Straight from a component ```typescript import { useQuery } from '@tanstack/react-query'; import { getPostQueryOptions, getAccountFullQueryOptions } from '@ecency/sdk'; const observer = currentAccount?.name; const { data: post, isLoading } = useQuery(getPostQueryOptions(author, permlink, observer)); const { data: account } = useQuery(getAccountFullQueryOptions(author)); ``` ## 2. App hook that adds mobile-only options The dominant shape: spread the SDK options, then override. Verbatim, `src/providers/queries/leaderboardQueries/leaderboardQueries.ts`: ```typescript import { useQuery } from '@tanstack/react-query'; import { getDiscoverLeaderboardQueryOptions } from '@ecency/sdk'; /** hook used to return leaderboard data using SDK */ export const useGetLeaderboardQuery = (duration: 'day' | 'week' | 'month') => { return useQuery({ ...getDiscoverLeaderboardQueryOptions(duration), // Opted in explicitly: the client default is off, because waking every query in // the cache on every resume is far more than this needs. The board lives inside a // tab that stays mounted, so without this it shows whatever it fetched hours ago // until the user thinks to pull to refresh. refetchOnWindowFocus: true, }); }; ``` Usual overrides: `enabled`, `select`, `staleTime`, `gcTime`, `initialData`. Keep the SDK's `queryKey` plus `queryFn` so the cache entry stays shared with other surfaces. ## 3. Private-API queries need the auth pair Ecency backend queries take `username` plus an access token. Use `useAuth()` rather than re-deriving it. From `src/providers/queries/newsletterQueries.ts`: ```typescript import { useAuth } from '../../hooks'; export const useDigestSubscriptionsQuery = () => { const { username, code } = useAuth(); return useQuery(getDigestSubscriptionsQueryOptions(username, code)); }; ``` ## 4. Infinite queries SDK `get*InfiniteQueryOptions` already carry `initialPageParam` plus `getNextPageParam`. The repo rarely hand-rolls those two. Flatten in the hook, do not re-key. From `src/providers/queries/draftQueries.ts` (comments stripped): ```typescript const { username, code } = useAuth(); const enabled = !!username && !!code; const infiniteQuery = useInfiniteQuery({ ...getDraftsInfiniteQueryOptions(username ?? '', code ?? '', limit), enabled, }); const data = useMemo(() => { if (!infiniteQuery.data?.pages) return []; return infiniteQuery.data.pages.flatMap((page) => page.data); }, [infiniteQuery.data?.pages]); return { ...infiniteQuery, data, pagesLoaded: infiniteQuery.data?.pages?.length ?? 0 }; ``` ## 5. Query keys - **SDK keys**: `import { QueryKeys } from '@ecency/sdk'`. Namespaced factories, not strings: `QueryKeys.posts.draftsInfinite(username, limit)`, `QueryKeys.accounts.full(name)`, `QueryKeys.polls.details(author, permlink)`. Use these to invalidate or seed an SDK cache entry. - **Mobile-only keys**: `src/providers/queries/queryKeys.ts` is a *default* export named `QUERIES` with a nested shape. Files import that default (`import QUERIES from '/queryKeys'`, so the specifier depends on the file) then `queryKey: [QUERIES.WALLET.GET_ACTIVITIES, username]`. There is no local `QueryKeys` export. ## 6. Hand-rolled query (last resort) Only when the SDK has nothing. Verbatim, `src/providers/queries/settingsQueries.ts`: ```typescript export const useGetServersQuery = () => { return useQuery({ queryKey: [QUERIES.SETTINGS.GET_SERVERS], queryFn: getNodes, placeholderData: [...SERVER_LIST], staleTime: 0, }); }; ``` `getNodes` comes from `src/providers/ecency/ecency.ts`, a provider module, not a bare fetch. ## 7. Export `src/providers/queries/index.ts` uses `export * from './Queries'`. It also re-exports `getQueryClient` from the SDK; the rest of the file is local (`initQueryClient` plus the persistence allowlist). A subdirectory carries its own `index.ts` that re-exports namespaces. ## Gotchas 1. **Persistence is an allowlist.** `_shouldDehydrateQuery` in `src/providers/queries/index.ts` switches on `queryKey[0]`, then narrows on `queryKey[1]`. A namespace with no case falls to the default and is dropped, so a new namespace is not persisted until you add its case. Within a case the subtype handling differs per namespace: some subtypes are dropped, some persist only while a single page is loaded, some persist wholesale. Read the switch before assuming a new key persists. 2. **Guard with `enabled`** when a param can be undefined. An `undefined` anywhere in a query key also blocks persistence. 3. Returning `undefined` from `getNextPageParam` stops pagination; that is what the repo's hand-rolled case returns. 4. **Optimistic vote data is no longer Redux.** Call `updateVoteInQueryCaches()` and read back via `applyRecentVoteOverrideToEntry()` from `src/providers/queries/postQueries/voteCacheUtils.ts`; seed a post before navigation with `usePostsCachePrimer()`. `useInjectVotesCache` is gone, though CLAUDE.md's Post Data Flow still lists it. 5. **Non-React code**: import `getQueryClient` from the app barrel `providers/queries` rather than the SDK: `await queryClient.fetchQuery(getAccountsQueryOptions([username]))`. 6. `src/providers/queries/sdk-config.ts` runs from `initQueryClient()` and configures `ConfigManager` (query client, private API host, image host, Hive nodes, DMCA lists). Adding a query rarely requires touching it. Prettier width is 100 (`.prettierrc`). Finish with `yarn lint`, `yarn typecheck` plus `yarn test:ci`; `.github/workflows/test.yml` runs all three on every PR. The baseline in `tsc-baseline.json` is empty, so any type error fails. Two co-located tests in `src/providers/queries/` read `index.ts` and assert the `export * from './Queries';` line from section 7, so a missing barrel export fails jest.