Skip to content

Commit 6298987

Browse files
committed
memoize useQuickActions internally, make linter check it
1 parent 619fd5e commit 6298987

19 files changed

Lines changed: 227 additions & 247 deletions

.oxlintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"react/button-has-type": "error",
3737
"react/jsx-boolean-value": "error",
3838

39-
"react-hooks/exhaustive-deps": "error",
39+
"react-hooks/exhaustive-deps": ["error", { "additionalHooks": "useQuickActions" }],
4040
"react-hooks/rules-of-hooks": "error",
4141
"import/no-default-export": "error",
4242
"consistent-type-imports": "error",

app/hooks/use-quick-actions.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,22 @@ function useParentActions(): QuickActionItem[] {
101101
}
102102

103103
/**
104-
* Register action items with the global quick actions menu. `items` must
105-
* be memoized by the caller, otherwise the effect will run too often.
104+
* Register action items with the global quick actions menu. Takes a factory
105+
* and deps array like useMemo — the linter checks deps via additionalHooks.
106106
*
107107
* Each component instance gets its own source slot (via useId), so items are
108108
* cleaned up automatically when the component unmounts without affecting
109109
* other sources' registrations.
110110
*/
111-
export function useQuickActions(items: QuickActionItem[]) {
111+
export function useQuickActions(
112+
factory: () => QuickActionItem[],
113+
deps: React.DependencyList
114+
) {
112115
const sourceId = useId()
116+
// Deps are checked by the linter at call sites via the additionalHooks
117+
// option in .oxlintrc.json, so we can safely forward them here.
118+
// eslint-disable-next-line react-hooks/exhaustive-deps
119+
const items = useMemo(factory, deps)
113120

114121
useEffect(() => {
115122
setSourceItems(sourceId, items)

app/layouts/ProjectLayoutBase.tsx

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { useMemo, type ReactElement } from 'react'
8+
import type { ReactElement } from 'react'
99
import { useLocation, type LoaderFunctionArgs } from 'react-router'
1010

1111
import { api, q, queryClient, usePrefetchedQuery } from '@oxide/api'
@@ -58,28 +58,27 @@ export function ProjectLayoutBase({ overrideContentPane }: ProjectLayoutProps) {
5858
const { data: project } = usePrefetchedQuery(projectView(projectSelector))
5959

6060
const { pathname } = useLocation()
61+
6162
useQuickActions(
62-
useMemo(
63-
() =>
64-
[
65-
{ value: 'Instances', path: pb.instances(projectSelector) },
66-
{ value: 'Disks', path: pb.disks(projectSelector) },
67-
{ value: 'Snapshots', path: pb.snapshots(projectSelector) },
68-
{ value: 'Images', path: pb.projectImages(projectSelector) },
69-
{ value: 'VPCs', path: pb.vpcs(projectSelector) },
70-
{ value: 'Floating IPs', path: pb.floatingIps(projectSelector) },
71-
{ value: 'Affinity Groups', path: pb.affinity(projectSelector) },
72-
{ value: 'Project Access', path: pb.projectAccess(projectSelector) },
73-
]
74-
// filter out the entry for the path we're currently on
75-
.filter((i) => i.path !== pathname)
76-
.map((i) => ({
77-
navGroup: `Project '${project.name}'`,
78-
value: i.value,
79-
action: i.path,
80-
})),
81-
[pathname, project.name, projectSelector]
82-
)
63+
() =>
64+
[
65+
{ value: 'Instances', path: pb.instances(projectSelector) },
66+
{ value: 'Disks', path: pb.disks(projectSelector) },
67+
{ value: 'Snapshots', path: pb.snapshots(projectSelector) },
68+
{ value: 'Images', path: pb.projectImages(projectSelector) },
69+
{ value: 'VPCs', path: pb.vpcs(projectSelector) },
70+
{ value: 'Floating IPs', path: pb.floatingIps(projectSelector) },
71+
{ value: 'Affinity Groups', path: pb.affinity(projectSelector) },
72+
{ value: 'Project Access', path: pb.projectAccess(projectSelector) },
73+
]
74+
// filter out the entry for the path we're currently on
75+
.filter((i) => i.path !== pathname)
76+
.map((i) => ({
77+
navGroup: `Project '${project.name}'`,
78+
value: i.value,
79+
action: i.path,
80+
})),
81+
[pathname, project.name, projectSelector]
8382
)
8483

8584
return (

app/layouts/SettingsLayout.tsx

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { useMemo } from 'react'
98
import { useLocation } from 'react-router'
109

1110
import {
@@ -30,22 +29,20 @@ export default function SettingsLayout() {
3029
const { pathname } = useLocation()
3130

3231
useQuickActions(
33-
useMemo(
34-
() =>
35-
[
36-
{ value: 'Profile', path: pb.profile() },
37-
{ value: 'SSH Keys', path: pb.sshKeys() },
38-
{ value: 'Access Tokens', path: pb.accessTokens() },
39-
]
40-
// filter out the entry for the path we're currently on
41-
.filter((i) => i.path !== pathname)
42-
.map((i) => ({
43-
navGroup: `Settings`,
44-
value: i.value,
45-
action: i.path,
46-
})),
47-
[pathname]
48-
)
32+
() =>
33+
[
34+
{ value: 'Profile', path: pb.profile() },
35+
{ value: 'SSH Keys', path: pb.sshKeys() },
36+
{ value: 'Access Tokens', path: pb.accessTokens() },
37+
]
38+
// filter out the entry for the path we're currently on
39+
.filter((i) => i.path !== pathname)
40+
.map((i) => ({
41+
navGroup: `Settings`,
42+
value: i.value,
43+
action: i.path,
44+
})),
45+
[pathname]
4946
)
5047

5148
return (

app/layouts/SiloLayout.tsx

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { useMemo } from 'react'
98
import { useLocation } from 'react-router'
109

1110
import {
@@ -29,23 +28,21 @@ export default function SiloLayout() {
2928
const { me } = useCurrentUser()
3029

3130
useQuickActions(
32-
useMemo(
33-
() =>
34-
[
35-
{ value: 'Projects', path: pb.projects() },
36-
{ value: 'Images', path: pb.siloImages() },
37-
{ value: 'Utilization', path: pb.siloUtilization() },
38-
{ value: 'Silo Access', path: pb.siloAccess() },
39-
]
40-
// filter out the entry for the path we're currently on
41-
.filter((i) => i.path !== pathname)
42-
.map((i) => ({
43-
navGroup: `Silo '${me.siloName}'`,
44-
value: i.value,
45-
action: i.path,
46-
})),
47-
[pathname, me.siloName]
48-
)
31+
() =>
32+
[
33+
{ value: 'Projects', path: pb.projects() },
34+
{ value: 'Images', path: pb.siloImages() },
35+
{ value: 'Utilization', path: pb.siloUtilization() },
36+
{ value: 'Silo Access', path: pb.siloAccess() },
37+
]
38+
// filter out the entry for the path we're currently on
39+
.filter((i) => i.path !== pathname)
40+
.map((i) => ({
41+
navGroup: `Silo '${me.siloName}'`,
42+
value: i.value,
43+
action: i.path,
44+
})),
45+
[pathname, me.siloName]
4946
)
5047

5148
return (

app/layouts/SystemLayout.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { useMemo } from 'react'
98
import { useLocation } from 'react-router'
109

1110
import { api, q, queryClient } from '@oxide/api'
@@ -48,7 +47,7 @@ export default function SystemLayout() {
4847

4948
const { me } = useCurrentUser()
5049

51-
const actions = useMemo(() => {
50+
useQuickActions(() => {
5251
const systemLinks = [
5352
{ value: 'Silos', path: pb.silos() },
5453
{ value: 'Utilization', path: pb.systemUtilization() },
@@ -73,8 +72,6 @@ export default function SystemLayout() {
7372
return [...systemLinks, backToSilo]
7473
}, [pathname, me.siloName])
7574

76-
useQuickActions(actions)
77-
7875
return (
7976
<PageContainer>
8077
<TopBar systemOrSilo="system" />

app/pages/ProjectsPage.tsx

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88
import { useQuery } from '@tanstack/react-query'
99
import { createColumnHelper } from '@tanstack/react-table'
10-
import { useCallback, useMemo } from 'react'
10+
import { useCallback } from 'react'
1111
import { Outlet, useNavigate } from 'react-router'
1212

1313
import { api, getListQFn, q, queryClient, useApiMutation, type Project } from '@oxide/api'
@@ -103,22 +103,21 @@ export default function ProjectsPage() {
103103
})
104104

105105
const { data: allProjects } = useQuery(q(api.projectList, { query: { limit: ALL_ISH } }))
106+
106107
useQuickActions(
107-
useMemo(
108-
() => [
109-
{
110-
value: 'New project',
111-
navGroup: 'Actions',
112-
action: pb.projectsNew(),
113-
},
114-
...(allProjects?.items || []).map((p) => ({
115-
value: p.name,
116-
action: pb.project({ project: p.name }),
117-
navGroup: 'Go to project',
118-
})),
119-
],
120-
[allProjects]
121-
)
108+
() => [
109+
{
110+
value: 'New project',
111+
navGroup: 'Actions',
112+
action: pb.projectsNew(),
113+
},
114+
...(allProjects?.items || []).map((p) => ({
115+
value: p.name,
116+
action: pb.project({ project: p.name }),
117+
navGroup: 'Go to project',
118+
})),
119+
],
120+
[allProjects]
122121
)
123122

124123
return (

app/pages/SiloAccessPage.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -166,16 +166,14 @@ export default function SiloAccessPage() {
166166
})
167167

168168
useQuickActions(
169-
useMemo(
170-
() => [
171-
{
172-
value: 'Add user or group',
173-
navGroup: 'Actions',
174-
action: () => setAddModalOpen(true),
175-
},
176-
],
177-
[]
178-
)
169+
() => [
170+
{
171+
value: 'Add user or group',
172+
navGroup: 'Actions',
173+
action: () => setAddModalOpen(true),
174+
},
175+
],
176+
[]
179177
)
180178

181179
return (

app/pages/SiloImagesPage.tsx

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -97,22 +97,21 @@ export default function SiloImagesPage() {
9797
const { table } = useQueryTable({ query: imageList, columns, emptyState: <EmptyState /> })
9898

9999
const { data: allImages } = useQuery(q(api.imageList, { query: { limit: ALL_ISH } }))
100+
100101
useQuickActions(
101-
useMemo(
102-
() => [
103-
{
104-
value: 'Promote image',
105-
navGroup: 'Actions',
106-
action: () => setShowModal(true),
107-
},
108-
...(allImages?.items || []).map((i) => ({
109-
value: i.name,
110-
action: pb.siloImageEdit({ image: i.name }),
111-
navGroup: 'Go to silo image',
112-
})),
113-
],
114-
[allImages]
115-
)
102+
() => [
103+
{
104+
value: 'Promote image',
105+
navGroup: 'Actions',
106+
action: () => setShowModal(true),
107+
},
108+
...(allImages?.items || []).map((i) => ({
109+
value: i.name,
110+
action: pb.siloImageEdit({ image: i.name }),
111+
navGroup: 'Go to silo image',
112+
})),
113+
],
114+
[allImages]
116115
)
117116

118117
return (

app/pages/project/access/ProjectAccessPage.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -207,16 +207,14 @@ export default function ProjectAccessPage() {
207207
})
208208

209209
useQuickActions(
210-
useMemo(
211-
() => [
212-
{
213-
value: 'Add user or group',
214-
navGroup: 'Actions',
215-
action: () => setAddModalOpen(true),
216-
},
217-
],
218-
[]
219-
)
210+
() => [
211+
{
212+
value: 'Add user or group',
213+
navGroup: 'Actions',
214+
action: () => setAddModalOpen(true),
215+
},
216+
],
217+
[]
220218
)
221219

222220
return (

0 commit comments

Comments
 (0)