55 *
66 * Copyright Oxide Computer Company
77 */
8- import type { ColumnDef } from '@tanstack/react-table'
8+ import type { CellContext , ColumnDef } from '@tanstack/react-table'
99import cn from 'classnames'
1010import { useMemo } from 'react'
1111
@@ -35,13 +35,15 @@ type MenuActionLink = MenuActionBase & {
3535 */
3636export type MenuAction = MenuActionItem | MenuActionLink
3737
38- type MakeActions < Item > = ( item : Item ) => Array < MenuAction >
38+ export type MakeActions < Item > = ( item : Item ) => Array < MenuAction >
3939
40- /** Convenience helper to combine regular cols with actions col and memoize */
40+ /**
41+ * Convenience helper to combine regular cols with actions col and memoize.
42+ * Memoizing `columns` and `makeActions` at the call site avoids re-renders but
43+ * is no longer required for correctness (see `ActionsCell`).
44+ */
4145export function useColsWithActions < TData extends Record < string , unknown > > (
42- /** Should be static or memoized */
4346 columns : ColumnDef < TData , any > [ ] , // eslint-disable-line @typescript-eslint/no-explicit-any
44- /** Must be memoized to avoid re-renders */
4547 makeActions : MakeActions < TData > ,
4648 copyIdLabel ?: string
4749) {
@@ -61,17 +63,34 @@ export const getActionsCol = <TData extends Record<string, unknown>>(
6163 meta : {
6264 thClassName : 'action-col' ,
6365 tdClassName : 'action-col' ,
66+ makeActions,
67+ copyIdLabel,
6468 } ,
65-
66- cell : ( { row } ) => {
67- // TODO: control flow here has always confused me, would like to straighten it out
68- const actions = makeActions ( row . original )
69- const id = typeof row . original . id === 'string' ? row . original . id : null
70- return < RowActions id = { id } actions = { actions } copyIdLabel = { copyIdLabel } />
71- } ,
69+ cell : ActionsCell ,
7270 }
7371}
7472
73+ /**
74+ * `flexRender` renders a function `cell` as a component, so its identity is
75+ * the element type. Passing `makeActions` through `meta` instead of a closure
76+ * keeps the type stable when `makeActions` changes (e.g., a query it depends
77+ * on refetches), which would otherwise remount the cell and close an open menu.
78+ *
79+ * This is why polling used to close row action menus and why call sites had to
80+ * memoize `makeActions` and `columns` so carefully (#2112, #2460, #2816). Those
81+ * memoizations are now only a render-count optimization.
82+ */
83+ function ActionsCell < TData extends Record < string , unknown > > ( {
84+ row,
85+ column,
86+ } : CellContext < TData , unknown > ) {
87+ // TODO: control flow here has always confused me, would like to straighten it out
88+ const { makeActions, copyIdLabel } = column . columnDef . meta ?? { }
89+ const actions = makeActions ?.( row . original )
90+ const id = typeof row . original . id === 'string' ? row . original . id : null
91+ return < RowActions id = { id } actions = { actions } copyIdLabel = { copyIdLabel } />
92+ }
93+
7594type RowActionsProps = {
7695 /** If `id` is provided, a `Copy ID` menu item will be automatically included. */
7796 id ?: string | null
0 commit comments