Skip to content

Commit

Permalink
refactor: start getting rid of apiQueryClient (#2597)
Browse files Browse the repository at this point in the history
* 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!
  • Loading branch information
david-crespo authored Dec 6, 2024
1 parent 243c55d commit 01a0ac9
Show file tree
Hide file tree
Showing 16 changed files with 153 additions and 197 deletions.
24 changes: 23 additions & 1 deletion app/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
*
* Copyright Oxide Computer Company
*/
import { QueryClient, useQuery, type UseQueryOptions } from '@tanstack/react-query'
import {
QueryClient as QueryClientOrig,
useQuery,
type UseQueryOptions,
} from '@tanstack/react-query'

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

/**
* Extends React Query's `QueryClient` with a couple of API-specific methods.
* Existing methods are never modified.
*/
class QueryClient extends QueryClientOrig {
/**
* Invalidate all cached queries for a given endpoint.
*
* Note that we only take a single argument, `method`, rather than allowing
* the full query key `[query, params]` to be specified. This is to avoid
* accidentally overspecifying and therefore failing to match the desired query.
* The params argument can be added in if we ever have a use case for it.
*/
invalidateEndpoint(method: keyof typeof api.methods) {
this.invalidateQueries({ queryKey: [method] })
}
}

// Needs to be defined here instead of in app so we can use it to define
// `apiQueryClient`, which provides API-typed versions of QueryClient methods
export const queryClient = new QueryClient({
Expand Down
21 changes: 8 additions & 13 deletions app/forms/floating-ip-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,27 @@
import { useForm } from 'react-hook-form'
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'

import {
apiQueryClient,
useApiMutation,
useApiQueryClient,
usePrefetchedApiQuery,
} from '@oxide/api'
import { apiq, queryClient, useApiMutation, usePrefetchedApiQuery } from '@oxide/api'

import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { NameField } from '~/components/form/fields/NameField'
import { SideModalForm } from '~/components/form/SideModalForm'
import { HL } from '~/components/HL'
import { getFloatingIpSelector, useFloatingIpSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import type * as PP from '~/util/path-params'
import { pb } from 'app/util/path-builder'

const floatingIpView = ({ project, floatingIp }: PP.FloatingIp) =>
apiq('floatingIpView', { path: { floatingIp }, query: { project } })

EditFloatingIpSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
const { floatingIp, project } = getFloatingIpSelector(params)
await apiQueryClient.prefetchQuery('floatingIpView', {
path: { floatingIp },
query: { project },
})
const selector = getFloatingIpSelector(params)
await queryClient.prefetchQuery(floatingIpView(selector))
return null
}

export function EditFloatingIpSideModalForm() {
const queryClient = useApiQueryClient()
const navigate = useNavigate()

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

const editFloatingIp = useApiMutation('floatingIpUpdate', {
onSuccess(_floatingIp) {
queryClient.invalidateQueries('floatingIpList')
queryClient.invalidateEndpoint('floatingIpList')
addToast(<>Floating IP <HL>{_floatingIp.name}</HL> updated</>) // prettier-ignore
onDismiss()
},
Expand Down
23 changes: 10 additions & 13 deletions app/forms/image-from-snapshot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import { useForm } from 'react-hook-form'
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'

import {
apiQueryClient,
apiq,
queryClient,
useApiMutation,
useApiQueryClient,
usePrefetchedApiQuery,
usePrefetchedQuery,
type ImageCreate,
} from '@oxide/api'

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

const defaultValues: Omit<ImageCreate, 'source'> = {
name: '',
Expand All @@ -34,29 +35,25 @@ const defaultValues: Omit<ImageCreate, 'source'> = {
version: '',
}

const snapshotView = ({ project, snapshot }: PP.Snapshot) =>
apiq('snapshotView', { path: { snapshot }, query: { project } })

CreateImageFromSnapshotSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
const { project, snapshot } = getProjectSnapshotSelector(params)
await apiQueryClient.prefetchQuery('snapshotView', {
path: { snapshot },
query: { project },
})
await queryClient.prefetchQuery(snapshotView({ project, snapshot }))
return null
}

export function CreateImageFromSnapshotSideModalForm() {
const { snapshot, project } = useProjectSnapshotSelector()
const { data } = usePrefetchedApiQuery('snapshotView', {
path: { snapshot },
query: { project },
})
const { data } = usePrefetchedQuery(snapshotView({ project, snapshot }))
const navigate = useNavigate()
const queryClient = useApiQueryClient()

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

const createImage = useApiMutation('imageCreate', {
onSuccess(image) {
queryClient.invalidateQueries('imageList')
queryClient.invalidateEndpoint('imageList')
addToast(<>Image <HL>{image.name}</HL> created</>) // prettier-ignore
onDismiss()
},
Expand Down
21 changes: 9 additions & 12 deletions app/forms/project-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@
import { useForm } from 'react-hook-form'
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'

import {
apiQueryClient,
useApiMutation,
useApiQueryClient,
usePrefetchedApiQuery,
} from '@oxide/api'
import { apiq, queryClient, useApiMutation, usePrefetchedQuery } from '@oxide/api'

import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { NameField } from '~/components/form/fields/NameField'
Expand All @@ -22,30 +17,32 @@ import { HL } from '~/components/HL'
import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import { pb } from '~/util/path-builder'
import type * as PP from '~/util/path-params'

const projectView = ({ project }: PP.Project) => apiq('projectView', { path: { project } })

EditProjectSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
const { project } = getProjectSelector(params)
await apiQueryClient.prefetchQuery('projectView', { path: { project } })
await queryClient.prefetchQuery(projectView({ project }))
return null
}

export function EditProjectSideModalForm() {
const queryClient = useApiQueryClient()
const navigate = useNavigate()

const projectSelector = useProjectSelector()

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

const { data: project } = usePrefetchedApiQuery('projectView', { path: projectSelector })
const { data: project } = usePrefetchedQuery(projectView(projectSelector))

const editProject = useApiMutation('projectUpdate', {
onSuccess(project) {
// refetch list of projects in sidebar
// TODO: check this invalidation
queryClient.invalidateQueries('projectList')
queryClient.invalidateEndpoint('projectList')
// avoid the project fetch when the project page loads since we have the data
queryClient.setQueryData('projectView', { path: { project: project.name } }, project)
const { queryKey } = projectView({ project: project.name })
queryClient.setQueryData(queryKey, project)
addToast(<>Project <HL>{project.name}</HL> updated</>) // prettier-ignore
onDismiss()
},
Expand Down
28 changes: 13 additions & 15 deletions app/forms/subnet-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { useForm } from 'react-hook-form'
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'

import {
apiQueryClient,
apiq,
queryClient,
useApiMutation,
useApiQueryClient,
usePrefetchedApiQuery,
usePrefetchedQuery,
type VpcSubnetUpdate,
} from '@oxide/api'

Expand All @@ -30,31 +30,29 @@ import { getVpcSubnetSelector, useVpcSubnetSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import { FormDivider } from '~/ui/lib/Divider'
import { pb } from '~/util/path-builder'
import type * as PP from '~/util/path-params'

const subnetView = ({ project, vpc, subnet }: PP.VpcSubnet) =>
apiq('vpcSubnetView', { query: { project, vpc }, path: { subnet } })

EditSubnetForm.loader = async ({ params }: LoaderFunctionArgs) => {
const { project, vpc, subnet } = getVpcSubnetSelector(params)
await apiQueryClient.prefetchQuery('vpcSubnetView', {
query: { project, vpc },
path: { subnet },
})
const selector = getVpcSubnetSelector(params)
await queryClient.prefetchQuery(subnetView(selector))
return null
}

export function EditSubnetForm() {
const { project, vpc, subnet: subnetName } = useVpcSubnetSelector()
const queryClient = useApiQueryClient()
const subnetSelector = useVpcSubnetSelector()
const { project, vpc } = subnetSelector

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

const { data: subnet } = usePrefetchedApiQuery('vpcSubnetView', {
query: { project, vpc },
path: { subnet: subnetName },
})
const { data: subnet } = usePrefetchedQuery(subnetView(subnetSelector))

const updateSubnet = useApiMutation('vpcSubnetUpdate', {
onSuccess(subnet) {
queryClient.invalidateQueries('vpcSubnetList')
queryClient.invalidateEndpoint('vpcSubnetList')
addToast(<>Subnet <HL>{subnet.name}</HL> updated</>) // prettier-ignore
onDismiss()
},
Expand Down
23 changes: 9 additions & 14 deletions app/forms/vpc-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@
import { useForm } from 'react-hook-form'
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'

import {
apiQueryClient,
useApiMutation,
useApiQueryClient,
usePrefetchedApiQuery,
} from '@oxide/api'
import { apiq, queryClient, useApiMutation, usePrefetchedQuery } from '@oxide/api'

import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { NameField } from '~/components/form/fields/NameField'
Expand All @@ -22,26 +17,26 @@ import { HL } from '~/components/HL'
import { getVpcSelector, useVpcSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import { pb } from '~/util/path-builder'
import type * as PP from '~/util/path-params'

const vpcView = ({ project, vpc }: PP.Vpc) =>
apiq('vpcView', { path: { vpc }, query: { project } })

EditVpcSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
const { project, vpc } = getVpcSelector(params)
await apiQueryClient.prefetchQuery('vpcView', { path: { vpc }, query: { project } })
await queryClient.prefetchQuery(vpcView({ project, vpc }))
return null
}

export function EditVpcSideModalForm() {
const { vpc: vpcName, project } = useVpcSelector()
const queryClient = useApiQueryClient()
const navigate = useNavigate()

const { data: vpc } = usePrefetchedApiQuery('vpcView', {
path: { vpc: vpcName },
query: { project },
})
const { data: vpc } = usePrefetchedQuery(vpcView({ project, vpc: vpcName }))

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

Expand All @@ -51,7 +46,7 @@ export function EditVpcSideModalForm() {
// page's VPC gets cleared out while we're still on the page. If we're
// navigating to a different page, its query will fetch anew regardless.
if (vpc.name === updatedVpc.name) {
queryClient.invalidateQueries('vpcView')
queryClient.invalidateEndpoint('vpcView')
}
},
})
Expand Down
25 changes: 11 additions & 14 deletions app/forms/vpc-router-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import {
} from 'react-router-dom'

import {
apiQueryClient,
apiq,
queryClient,
useApiMutation,
useApiQueryClient,
usePrefetchedApiQuery,
usePrefetchedQuery,
type VpcRouterUpdate,
} from '@oxide/api'

Expand All @@ -27,24 +27,21 @@ import { HL } from '~/components/HL'
import { getVpcRouterSelector, useVpcRouterSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import { pb } from '~/util/path-builder'
import type * as PP from '~/util/path-params'

const routerView = ({ project, vpc, router }: PP.VpcRouter) =>
apiq('vpcRouterView', { path: { router }, query: { project, vpc } })

EditRouterSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
const { router, project, vpc } = getVpcRouterSelector(params)
await apiQueryClient.prefetchQuery('vpcRouterView', {
path: { router },
query: { project, vpc },
})
const selector = getVpcRouterSelector(params)
await queryClient.prefetchQuery(routerView(selector))
return null
}

export function EditRouterSideModalForm() {
const queryClient = useApiQueryClient()
const routerSelector = useVpcRouterSelector()
const { project, vpc, router } = routerSelector
const { data: routerData } = usePrefetchedApiQuery('vpcRouterView', {
path: { router },
query: { project, vpc },
})
const { data: routerData } = usePrefetchedQuery(routerView(routerSelector))
const navigate = useNavigate()

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

const editRouter = useApiMutation('vpcRouterUpdate', {
onSuccess(updatedRouter) {
queryClient.invalidateQueries('vpcRouterList')
queryClient.invalidateEndpoint('vpcRouterList')
addToast(<>Router <HL>{updatedRouter.name}</HL> updated</>) // prettier-ignore
navigate(pb.vpcRouters({ project, vpc }))
},
Expand Down
Loading

0 comments on commit 01a0ac9

Please sign in to comment.