diff --git a/AGENTS.md b/AGENTS.md index 28ab25e95..6681fc422 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,11 +36,11 @@ # Data fetching pattern -- Data from `usePrefetchedQuery` is guaranteed to be defined (the loader ensures it and the hook throws if it's not present). Do not add `if (!data) return` guards on these values. +- `usePrefetchedQuery` requires that the loader fetched and awaited the same query — the hook throws if the data isn't in the cache. Because of that guarantee, do not add `if (!data) return` guards on its results. If the loader's fetch is conditional or not awaited, the guarantee doesn't hold: use `useQuery` with a loading fallback instead. - Define queries with `q(api.endpoint, params)` for single items or `getListQFn(api.listEndpoint, params)` for lists. Prefetch in `clientLoader` and read with `usePrefetchedQuery`; for on-demand fetches (modals, secondary data), use `useQuery` directly. - Use `ALL_ISH` from `app/util/consts.ts` when UI needs "all" items. Use `queryClient.invalidateEndpoint` to invalidate queries. - For paginated tables, compose `getListQFn` with `useQueryTable`; the helper wraps `limit`/`pageToken` handling and keeps placeholder data stable (`app/api/hooks.ts:123-188`, `app/pages/ProjectsPage.tsx:40-132`). -- When a loader needs dependent data, fetch the primary list with `queryClient.fetchQuery`, prefetch its per-item queries, and only await a bounded batch so render isn't blocked (see `app/pages/project/affinity/AffinityPage.tsx`). +- When a loader needs per-item data for a list, await the list with `queryClient.fetchQuery`, then kick off `prefetchQuery` for each item without awaiting, so render isn't blocked. Read the per-item queries with `useQuery` and a skeleton fallback — they may not have resolved by first render (see `app/pages/project/affinity/AffinityPage.tsx`). - When modals need async data, fetch with `queryClient.ensureQueryData` before opening the modal so cached data is reused and there's no content pop-in. - Use `qErrorsAllowed` in loaders for endpoints where some users may lack permission, so the page degrades gracefully instead of the loader throwing (see `SiloScimTab.tsx`). diff --git a/app/pages/project/affinity/AffinityPage.tsx b/app/pages/project/affinity/AffinityPage.tsx index 7122e494c..b8c83d7c8 100644 --- a/app/pages/project/affinity/AffinityPage.tsx +++ b/app/pages/project/affinity/AffinityPage.tsx @@ -6,6 +6,7 @@ * Copyright Oxide Computer Company */ +import { useQuery } from '@tanstack/react-query' import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table' import { useCallback } from 'react' import { Outlet, type LoaderFunctionArgs } from 'react-router' @@ -42,15 +43,15 @@ import { pb } from '~/util/path-builder' export async function clientLoader({ params }: LoaderFunctionArgs) { const { project } = getProjectSelector(params) const groups = await queryClient.fetchQuery(antiAffinityGroupList({ project })) - const memberFetches = groups.items.map(({ name }) => + // Warm the cache for each group's member count in parallel as the route loads, + // but don't block render on them: the count cells read with useQuery and show a + // skeleton until their list arrives. This keeps the page responsive no matter + // how many groups there are. + for (const { name } of groups.items) { queryClient.prefetchQuery( antiAffinityGroupMemberList({ antiAffinityGroup: name, project }) ) - ) - // The browser will fetch up to 6 anti-affinity group member lists without queuing, - // so we can prefetch them without slowing down the page. If there are more than 6 groups, - // we won't bother to wait for the promises to fulfill, and will just load the actual page content. - if (groups.items.length < 6) await Promise.all(memberFetches) + } return null } @@ -190,7 +191,10 @@ export const AffinityGroupMembersCell = ({ antiAffinityGroup: string }) => { const { project } = useProjectSelector() - const { data: members } = usePrefetchedQuery( + // Not usePrefetchedQuery: the loader prefetches these member lists without + // awaiting them, so they may not be in cache at render time. useQuery lets us + // show a skeleton instead of tripping the prefetch invariant. + const { data: members } = useQuery( antiAffinityGroupMemberList({ antiAffinityGroup, project }) ) if (!members) return