Skip to content

Commit 01a0ac9

Browse files
authored
refactor: start getting rid of apiQueryClient (#2597)
* show what it looks like to get rid of apiQueryClient * try invalidateOptions on list options helper * show what it looks like to get apiQueryClient out of more pages * put invalidate function on queryClient by extending class * convert a few more to see how they look * use the new path params types * remove invalidateOptions on paginated query thing * convert some more, actually cut some lines!
1 parent 243c55d commit 01a0ac9

File tree

16 files changed

+153
-197
lines changed

16 files changed

+153
-197
lines changed

app/api/client.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { QueryClient, useQuery, type UseQueryOptions } from '@tanstack/react-query'
8+
import {
9+
QueryClient as QueryClientOrig,
10+
useQuery,
11+
type UseQueryOptions,
12+
} from '@tanstack/react-query'
913

1014
import { Api } from './__generated__/Api'
1115
import { type ApiError } from './errors'
@@ -49,6 +53,24 @@ export const useApiMutation = getUseApiMutation(api.methods)
4953
export const usePrefetchedQuery = <TData>(options: UseQueryOptions<TData, ApiError>) =>
5054
ensurePrefetched(useQuery(options), options.queryKey)
5155

56+
/**
57+
* Extends React Query's `QueryClient` with a couple of API-specific methods.
58+
* Existing methods are never modified.
59+
*/
60+
class QueryClient extends QueryClientOrig {
61+
/**
62+
* Invalidate all cached queries for a given endpoint.
63+
*
64+
* Note that we only take a single argument, `method`, rather than allowing
65+
* the full query key `[query, params]` to be specified. This is to avoid
66+
* accidentally overspecifying and therefore failing to match the desired query.
67+
* The params argument can be added in if we ever have a use case for it.
68+
*/
69+
invalidateEndpoint(method: keyof typeof api.methods) {
70+
this.invalidateQueries({ queryKey: [method] })
71+
}
72+
}
73+
5274
// Needs to be defined here instead of in app so we can use it to define
5375
// `apiQueryClient`, which provides API-typed versions of QueryClient methods
5476
export const queryClient = new QueryClient({

app/forms/floating-ip-edit.tsx

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,27 @@
88
import { useForm } from 'react-hook-form'
99
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'
1010

11-
import {
12-
apiQueryClient,
13-
useApiMutation,
14-
useApiQueryClient,
15-
usePrefetchedApiQuery,
16-
} from '@oxide/api'
11+
import { apiq, queryClient, useApiMutation, usePrefetchedApiQuery } from '@oxide/api'
1712

1813
import { DescriptionField } from '~/components/form/fields/DescriptionField'
1914
import { NameField } from '~/components/form/fields/NameField'
2015
import { SideModalForm } from '~/components/form/SideModalForm'
2116
import { HL } from '~/components/HL'
2217
import { getFloatingIpSelector, useFloatingIpSelector } from '~/hooks/use-params'
2318
import { addToast } from '~/stores/toast'
19+
import type * as PP from '~/util/path-params'
2420
import { pb } from 'app/util/path-builder'
2521

22+
const floatingIpView = ({ project, floatingIp }: PP.FloatingIp) =>
23+
apiq('floatingIpView', { path: { floatingIp }, query: { project } })
24+
2625
EditFloatingIpSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
27-
const { floatingIp, project } = getFloatingIpSelector(params)
28-
await apiQueryClient.prefetchQuery('floatingIpView', {
29-
path: { floatingIp },
30-
query: { project },
31-
})
26+
const selector = getFloatingIpSelector(params)
27+
await queryClient.prefetchQuery(floatingIpView(selector))
3228
return null
3329
}
3430

3531
export function EditFloatingIpSideModalForm() {
36-
const queryClient = useApiQueryClient()
3732
const navigate = useNavigate()
3833

3934
const floatingIpSelector = useFloatingIpSelector()
@@ -47,7 +42,7 @@ export function EditFloatingIpSideModalForm() {
4742

4843
const editFloatingIp = useApiMutation('floatingIpUpdate', {
4944
onSuccess(_floatingIp) {
50-
queryClient.invalidateQueries('floatingIpList')
45+
queryClient.invalidateEndpoint('floatingIpList')
5146
addToast(<>Floating IP <HL>{_floatingIp.name}</HL> updated</>) // prettier-ignore
5247
onDismiss()
5348
},

app/forms/image-from-snapshot.tsx

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import { useForm } from 'react-hook-form'
1010
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'
1111

1212
import {
13-
apiQueryClient,
13+
apiq,
14+
queryClient,
1415
useApiMutation,
15-
useApiQueryClient,
16-
usePrefetchedApiQuery,
16+
usePrefetchedQuery,
1717
type ImageCreate,
1818
} from '@oxide/api'
1919

@@ -26,6 +26,7 @@ import { getProjectSnapshotSelector, useProjectSnapshotSelector } from '~/hooks/
2626
import { addToast } from '~/stores/toast'
2727
import { PropertiesTable } from '~/ui/lib/PropertiesTable'
2828
import { pb } from '~/util/path-builder'
29+
import type * as PP from '~/util/path-params'
2930

3031
const defaultValues: Omit<ImageCreate, 'source'> = {
3132
name: '',
@@ -34,29 +35,25 @@ const defaultValues: Omit<ImageCreate, 'source'> = {
3435
version: '',
3536
}
3637

38+
const snapshotView = ({ project, snapshot }: PP.Snapshot) =>
39+
apiq('snapshotView', { path: { snapshot }, query: { project } })
40+
3741
CreateImageFromSnapshotSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
3842
const { project, snapshot } = getProjectSnapshotSelector(params)
39-
await apiQueryClient.prefetchQuery('snapshotView', {
40-
path: { snapshot },
41-
query: { project },
42-
})
43+
await queryClient.prefetchQuery(snapshotView({ project, snapshot }))
4344
return null
4445
}
4546

4647
export function CreateImageFromSnapshotSideModalForm() {
4748
const { snapshot, project } = useProjectSnapshotSelector()
48-
const { data } = usePrefetchedApiQuery('snapshotView', {
49-
path: { snapshot },
50-
query: { project },
51-
})
49+
const { data } = usePrefetchedQuery(snapshotView({ project, snapshot }))
5250
const navigate = useNavigate()
53-
const queryClient = useApiQueryClient()
5451

5552
const onDismiss = () => navigate(pb.snapshots({ project }))
5653

5754
const createImage = useApiMutation('imageCreate', {
5855
onSuccess(image) {
59-
queryClient.invalidateQueries('imageList')
56+
queryClient.invalidateEndpoint('imageList')
6057
addToast(<>Image <HL>{image.name}</HL> created</>) // prettier-ignore
6158
onDismiss()
6259
},

app/forms/project-edit.tsx

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,7 @@
88
import { useForm } from 'react-hook-form'
99
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'
1010

11-
import {
12-
apiQueryClient,
13-
useApiMutation,
14-
useApiQueryClient,
15-
usePrefetchedApiQuery,
16-
} from '@oxide/api'
11+
import { apiq, queryClient, useApiMutation, usePrefetchedQuery } from '@oxide/api'
1712

1813
import { DescriptionField } from '~/components/form/fields/DescriptionField'
1914
import { NameField } from '~/components/form/fields/NameField'
@@ -22,30 +17,32 @@ import { HL } from '~/components/HL'
2217
import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
2318
import { addToast } from '~/stores/toast'
2419
import { pb } from '~/util/path-builder'
20+
import type * as PP from '~/util/path-params'
21+
22+
const projectView = ({ project }: PP.Project) => apiq('projectView', { path: { project } })
2523

2624
EditProjectSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
2725
const { project } = getProjectSelector(params)
28-
await apiQueryClient.prefetchQuery('projectView', { path: { project } })
26+
await queryClient.prefetchQuery(projectView({ project }))
2927
return null
3028
}
3129

3230
export function EditProjectSideModalForm() {
33-
const queryClient = useApiQueryClient()
3431
const navigate = useNavigate()
3532

3633
const projectSelector = useProjectSelector()
3734

3835
const onDismiss = () => navigate(pb.projects())
3936

40-
const { data: project } = usePrefetchedApiQuery('projectView', { path: projectSelector })
37+
const { data: project } = usePrefetchedQuery(projectView(projectSelector))
4138

4239
const editProject = useApiMutation('projectUpdate', {
4340
onSuccess(project) {
4441
// refetch list of projects in sidebar
45-
// TODO: check this invalidation
46-
queryClient.invalidateQueries('projectList')
42+
queryClient.invalidateEndpoint('projectList')
4743
// avoid the project fetch when the project page loads since we have the data
48-
queryClient.setQueryData('projectView', { path: { project: project.name } }, project)
44+
const { queryKey } = projectView({ project: project.name })
45+
queryClient.setQueryData(queryKey, project)
4946
addToast(<>Project <HL>{project.name}</HL> updated</>) // prettier-ignore
5047
onDismiss()
5148
},

app/forms/subnet-edit.tsx

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import { useForm } from 'react-hook-form'
99
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'
1010

1111
import {
12-
apiQueryClient,
12+
apiq,
13+
queryClient,
1314
useApiMutation,
14-
useApiQueryClient,
15-
usePrefetchedApiQuery,
15+
usePrefetchedQuery,
1616
type VpcSubnetUpdate,
1717
} from '@oxide/api'
1818

@@ -30,31 +30,29 @@ import { getVpcSubnetSelector, useVpcSubnetSelector } from '~/hooks/use-params'
3030
import { addToast } from '~/stores/toast'
3131
import { FormDivider } from '~/ui/lib/Divider'
3232
import { pb } from '~/util/path-builder'
33+
import type * as PP from '~/util/path-params'
34+
35+
const subnetView = ({ project, vpc, subnet }: PP.VpcSubnet) =>
36+
apiq('vpcSubnetView', { query: { project, vpc }, path: { subnet } })
3337

3438
EditSubnetForm.loader = async ({ params }: LoaderFunctionArgs) => {
35-
const { project, vpc, subnet } = getVpcSubnetSelector(params)
36-
await apiQueryClient.prefetchQuery('vpcSubnetView', {
37-
query: { project, vpc },
38-
path: { subnet },
39-
})
39+
const selector = getVpcSubnetSelector(params)
40+
await queryClient.prefetchQuery(subnetView(selector))
4041
return null
4142
}
4243

4344
export function EditSubnetForm() {
44-
const { project, vpc, subnet: subnetName } = useVpcSubnetSelector()
45-
const queryClient = useApiQueryClient()
45+
const subnetSelector = useVpcSubnetSelector()
46+
const { project, vpc } = subnetSelector
4647

4748
const navigate = useNavigate()
4849
const onDismiss = () => navigate(pb.vpcSubnets({ project, vpc }))
4950

50-
const { data: subnet } = usePrefetchedApiQuery('vpcSubnetView', {
51-
query: { project, vpc },
52-
path: { subnet: subnetName },
53-
})
51+
const { data: subnet } = usePrefetchedQuery(subnetView(subnetSelector))
5452

5553
const updateSubnet = useApiMutation('vpcSubnetUpdate', {
5654
onSuccess(subnet) {
57-
queryClient.invalidateQueries('vpcSubnetList')
55+
queryClient.invalidateEndpoint('vpcSubnetList')
5856
addToast(<>Subnet <HL>{subnet.name}</HL> updated</>) // prettier-ignore
5957
onDismiss()
6058
},

app/forms/vpc-edit.tsx

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,7 @@
88
import { useForm } from 'react-hook-form'
99
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'
1010

11-
import {
12-
apiQueryClient,
13-
useApiMutation,
14-
useApiQueryClient,
15-
usePrefetchedApiQuery,
16-
} from '@oxide/api'
11+
import { apiq, queryClient, useApiMutation, usePrefetchedQuery } from '@oxide/api'
1712

1813
import { DescriptionField } from '~/components/form/fields/DescriptionField'
1914
import { NameField } from '~/components/form/fields/NameField'
@@ -22,26 +17,26 @@ import { HL } from '~/components/HL'
2217
import { getVpcSelector, useVpcSelector } from '~/hooks/use-params'
2318
import { addToast } from '~/stores/toast'
2419
import { pb } from '~/util/path-builder'
20+
import type * as PP from '~/util/path-params'
21+
22+
const vpcView = ({ project, vpc }: PP.Vpc) =>
23+
apiq('vpcView', { path: { vpc }, query: { project } })
2524

2625
EditVpcSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
2726
const { project, vpc } = getVpcSelector(params)
28-
await apiQueryClient.prefetchQuery('vpcView', { path: { vpc }, query: { project } })
27+
await queryClient.prefetchQuery(vpcView({ project, vpc }))
2928
return null
3029
}
3130

3231
export function EditVpcSideModalForm() {
3332
const { vpc: vpcName, project } = useVpcSelector()
34-
const queryClient = useApiQueryClient()
3533
const navigate = useNavigate()
3634

37-
const { data: vpc } = usePrefetchedApiQuery('vpcView', {
38-
path: { vpc: vpcName },
39-
query: { project },
40-
})
35+
const { data: vpc } = usePrefetchedQuery(vpcView({ project, vpc: vpcName }))
4136

4237
const editVpc = useApiMutation('vpcUpdate', {
4338
onSuccess(updatedVpc) {
44-
queryClient.invalidateQueries('vpcList')
39+
queryClient.invalidateEndpoint('vpcList')
4540
navigate(pb.vpc({ project, vpc: updatedVpc.name }))
4641
addToast(<>VPC <HL>{updatedVpc.name}</HL> updated</>) // prettier-ignore
4742

@@ -51,7 +46,7 @@ export function EditVpcSideModalForm() {
5146
// page's VPC gets cleared out while we're still on the page. If we're
5247
// navigating to a different page, its query will fetch anew regardless.
5348
if (vpc.name === updatedVpc.name) {
54-
queryClient.invalidateQueries('vpcView')
49+
queryClient.invalidateEndpoint('vpcView')
5550
}
5651
},
5752
})

app/forms/vpc-router-edit.tsx

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import {
1313
} from 'react-router-dom'
1414

1515
import {
16-
apiQueryClient,
16+
apiq,
17+
queryClient,
1718
useApiMutation,
18-
useApiQueryClient,
19-
usePrefetchedApiQuery,
19+
usePrefetchedQuery,
2020
type VpcRouterUpdate,
2121
} from '@oxide/api'
2222

@@ -27,24 +27,21 @@ import { HL } from '~/components/HL'
2727
import { getVpcRouterSelector, useVpcRouterSelector } from '~/hooks/use-params'
2828
import { addToast } from '~/stores/toast'
2929
import { pb } from '~/util/path-builder'
30+
import type * as PP from '~/util/path-params'
31+
32+
const routerView = ({ project, vpc, router }: PP.VpcRouter) =>
33+
apiq('vpcRouterView', { path: { router }, query: { project, vpc } })
3034

3135
EditRouterSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
32-
const { router, project, vpc } = getVpcRouterSelector(params)
33-
await apiQueryClient.prefetchQuery('vpcRouterView', {
34-
path: { router },
35-
query: { project, vpc },
36-
})
36+
const selector = getVpcRouterSelector(params)
37+
await queryClient.prefetchQuery(routerView(selector))
3738
return null
3839
}
3940

4041
export function EditRouterSideModalForm() {
41-
const queryClient = useApiQueryClient()
4242
const routerSelector = useVpcRouterSelector()
4343
const { project, vpc, router } = routerSelector
44-
const { data: routerData } = usePrefetchedApiQuery('vpcRouterView', {
45-
path: { router },
46-
query: { project, vpc },
47-
})
44+
const { data: routerData } = usePrefetchedQuery(routerView(routerSelector))
4845
const navigate = useNavigate()
4946

5047
const onDismiss = (navigate: NavigateFunction) => {
@@ -53,7 +50,7 @@ export function EditRouterSideModalForm() {
5350

5451
const editRouter = useApiMutation('vpcRouterUpdate', {
5552
onSuccess(updatedRouter) {
56-
queryClient.invalidateQueries('vpcRouterList')
53+
queryClient.invalidateEndpoint('vpcRouterList')
5754
addToast(<>Router <HL>{updatedRouter.name}</HL> updated</>) // prettier-ignore
5855
navigate(pb.vpcRouters({ project, vpc }))
5956
},

0 commit comments

Comments
 (0)