Skip to content

Commit 67033a3

Browse files
authored
refactor: Use queryOptions API to clean up useApiQuery hook definitions (#2566)
refactor useApiQuery hooks to use queryOptions helper internally
1 parent c425880 commit 67033a3

File tree

2 files changed

+42
-27
lines changed

2 files changed

+42
-27
lines changed

app/api/hooks.ts

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@
66
* Copyright Oxide Computer Company
77
*/
88
import {
9+
hashKey,
10+
queryOptions,
911
useMutation,
1012
useQueries,
1113
useQuery,
1214
type DefaultError,
1315
type FetchQueryOptions,
1416
type InvalidateQueryFilters,
1517
type QueryClient,
18+
type QueryKey,
1619
type UndefinedInitialDataOptions,
1720
type UseMutationOptions,
1821
type UseQueryOptions,
22+
type UseQueryResult,
1923
} from '@tanstack/react-query'
2024
import { type SetNonNullable } from 'type-fest'
2125

@@ -100,14 +104,14 @@ type FetchQueryOtherOptions<T, E = DefaultError> = Omit<
100104
'queryKey' | 'queryFn'
101105
>
102106

103-
export const getUseApiQuery =
107+
export const getApiQueryOptions =
104108
<A extends ApiClient>(api: A) =>
105109
<M extends string & keyof A>(
106110
method: M,
107111
params: Params<A[M]>,
108112
options: UseQueryOtherOptions<Result<A[M]>, ApiError> = {}
109-
) => {
110-
return useQuery({
113+
) =>
114+
queryOptions({
111115
queryKey: [method, params],
112116
// no catch, let unexpected errors bubble up
113117
queryFn: ({ signal }) => api[method](params, { signal }).then(handleResult(method)),
@@ -118,7 +122,15 @@ export const getUseApiQuery =
118122
throwOnError: (err) => err.statusCode === 404,
119123
...options,
120124
})
121-
}
125+
126+
export const getUseApiQuery =
127+
<A extends ApiClient>(api: A) =>
128+
<M extends string & keyof A>(
129+
method: M,
130+
params: Params<A[M]>,
131+
options: UseQueryOtherOptions<Result<A[M]>, ApiError> = {}
132+
) =>
133+
useQuery(getApiQueryOptions(api)(method, params, options))
122134

123135
export const getUsePrefetchedApiQuery =
124136
<A extends ApiClient>(api: A) =>
@@ -127,33 +139,32 @@ export const getUsePrefetchedApiQuery =
127139
params: Params<A[M]>,
128140
options: UseQueryOtherOptions<Result<A[M]>, ApiError> = {}
129141
) => {
130-
const queryKey = [method, params]
131-
const result = useQuery({
132-
queryKey,
133-
// no catch, let unexpected errors bubble up
134-
queryFn: ({ signal }) => api[method](params, { signal }).then(handleResult(method)),
142+
const qOptions = getApiQueryOptions(api)(method, params, options)
143+
return ensure(useQuery(qOptions), qOptions.queryKey)
144+
}
135145

136-
// we can say Not Found. If you need to allow a 404 and want it to show
137-
// up as `error` state instead, pass `useErrorBoundary: false` as an
138-
// option from the calling component and it will override this
139-
throwOnError: (err) => err.statusCode === 404,
140-
...options,
141-
})
142-
invariant(
143-
result.data,
144-
`Expected query to be prefetched.
145-
Key: ${JSON.stringify(queryKey)}
146+
const prefetchError = (key?: QueryKey) =>
147+
`Expected query to be prefetched.
148+
Key: ${key ? hashKey(key) : '<unknown>'}
146149
Ensure the following:
147150
• loader is called in routes.tsx and is running
148151
• query matches in both the loader and the component
149152
• request isn't erroring-out server-side (check the Networking tab)
150-
• mock API endpoint is implemented in handlers.ts
151-
`
152-
)
153-
// TS infers non-nullable on a freestanding variable, but doesn't like to do
154-
// it on a property. So we give it a hint
155-
return result as SetNonNullable<typeof result, 'data'>
156-
}
153+
• mock API endpoint is implemented in handlers.ts`
154+
155+
export function ensure<TData, TError>(
156+
result: UseQueryResult<TData, TError>,
157+
/**
158+
* Optional because if we call this manually from a component like
159+
* `ensure(useQuery(...))`, * we don't necessarily have access to the key.
160+
*/
161+
key?: QueryKey
162+
) {
163+
invariant(result.data, prefetchError(key))
164+
// TS infers non-nullable on a freestanding variable, but doesn't like to do
165+
// it on a property. So we give it a hint
166+
return result as SetNonNullable<typeof result, 'data'>
167+
}
157168

158169
const ERRORS_ALLOWED = 'errors-allowed'
159170

test/e2e/breadcrumbs.e2e.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ test('breadcrumbs', async ({ page }) => {
4141
// the form route doesn't have its own crumb
4242
await page.getByRole('link', { name: 'New VPC' }).click()
4343
await expect(page).toHaveURL('/projects/mock-project/vpcs-new')
44+
await expect(page.getByRole('dialog', { name: 'Create VPC' })).toBeVisible()
4445
await expectCrumbs(page, vpcsCrumbs)
4546

4647
// try a nested one with a tab
4748
await page.goto('/projects/mock-project/instances/db1/networking')
49+
await expect(page.getByRole('tab', { name: 'Networking' })).toBeVisible()
4850
await expectCrumbs(page, [
4951
...projectCrumbs,
5052
['Instances', '/projects/mock-project/instances'],
@@ -77,6 +79,8 @@ test('breadcrumbs', async ({ page }) => {
7779
['ip-pool-1', '/system/networking/ip-pools/ip-pool-1'],
7880
]
7981
await expectCrumbs(page, poolCrumbs)
80-
await page.goto('/system/networking/ip-pools/ip-pool-1/ranges-add')
82+
await page.getByRole('link', { name: 'Add range' }).click()
83+
await expect(page).toHaveURL('/system/networking/ip-pools/ip-pool-1/ranges-add')
84+
await expect(page.getByRole('dialog', { name: 'Add IP range' })).toBeVisible()
8185
await expectCrumbs(page, poolCrumbs)
8286
})

0 commit comments

Comments
 (0)