Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).

Expand Down
18 changes: 11 additions & 7 deletions app/pages/project/affinity/AffinityPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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 <SkeletonCell />
Expand Down
Loading