From bfa85b983596181febd0ed2897e157b10f78f9ee Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:35:39 +0100 Subject: [PATCH] fix(base): scope per-base UI atoms by pageId to prevent embed render loops Two BaseTable instances on the same page (e.g. multiple base embeds in one document) shared the same global jotai atoms for activeViewId, editingCell, property menu state, and row selection. Each instance's useEffect that synced activeViewId would clobber the other's value every render, pinning React into a "Maximum update depth exceeded" loop. Convert every UI atom in base-atoms.ts to an atomFamily keyed by pageId so each base owns its own scope, and thread pageId through the grid component tree (GridRow, GridCell, GridHeaderCell, RowNumberCell, RowNumberHeaderCell, PropertyMenuContent) plus useRowSelection so each consumer reaches the per-base atom. use-base-socket already had pageId in scope; its store.get/store.set calls now resolve through selectedRowIdsAtomFamily(pageId) too. --- .../src/features/base/atoms/base-atoms.ts | 37 +++++++++++++++---- .../features/base/components/base-table.tsx | 6 +-- .../base/components/grid/grid-cell.tsx | 7 +++- .../base/components/grid/grid-container.tsx | 17 +++++---- .../base/components/grid/grid-header-cell.tsx | 19 ++++++---- .../base/components/grid/grid-header.tsx | 15 ++++---- .../base/components/grid/grid-row.tsx | 5 ++- .../base/components/grid/row-number-cell.tsx | 4 +- .../grid/row-number-header-cell.tsx | 4 +- .../components/grid/selection-action-bar.tsx | 2 +- .../components/property/property-menu.tsx | 8 ++-- .../features/base/hooks/use-base-socket.ts | 12 +++--- .../base/hooks/use-delete-selected-rows.tsx | 2 +- .../features/base/hooks/use-row-selection.ts | 12 +++--- 14 files changed, 96 insertions(+), 54 deletions(-) diff --git a/apps/client/src/features/base/atoms/base-atoms.ts b/apps/client/src/features/base/atoms/base-atoms.ts index 6dbf1bb17..247d1a475 100644 --- a/apps/client/src/features/base/atoms/base-atoms.ts +++ b/apps/client/src/features/base/atoms/base-atoms.ts @@ -1,15 +1,38 @@ import { atom } from "jotai"; +import { atomFamily } from "jotai/utils"; import { EditingCell } from "@/features/base/types/base.types"; -export const activeViewIdAtom = atom(null); +// Atoms are scoped per-base via `pageId` so that two BaseTable instances +// rendered on the same page (e.g. multiple base embeds inside one +// document) don't share UI state. A global atom would otherwise cause +// each instance's `useEffect` writers to clobber the other's value +// every render — pinning React into a "Maximum update depth exceeded" +// loop. -export const editingCellAtom = atom(null); +export const activeViewIdAtomFamily = atomFamily((_pageId: string) => + atom(null), +); -export const activePropertyMenuAtom = atom(null); +export const editingCellAtomFamily = atomFamily((_pageId: string) => + atom(null), +); -export const propertyMenuDirtyAtom = atom(false); +export const activePropertyMenuAtomFamily = atomFamily((_pageId: string) => + atom(null), +); -export const propertyMenuCloseRequestAtom = atom(0); +export const propertyMenuDirtyAtomFamily = atomFamily((_pageId: string) => + atom(false), +); -export const selectedRowIdsAtom = atom>(new Set()); -export const lastToggledRowIndexAtom = atom(null); +export const propertyMenuCloseRequestAtomFamily = atomFamily((_pageId: string) => + atom(0), +); + +export const selectedRowIdsAtomFamily = atomFamily((_pageId: string) => + atom>(new Set()), +); + +export const lastToggledRowIndexAtomFamily = atomFamily((_pageId: string) => + atom(null), +); diff --git a/apps/client/src/features/base/components/base-table.tsx b/apps/client/src/features/base/components/base-table.tsx index 16acdbbf7..393e38121 100644 --- a/apps/client/src/features/base/components/base-table.tsx +++ b/apps/client/src/features/base/components/base-table.tsx @@ -24,7 +24,7 @@ import { useCreateViewMutation, useUpdateViewMutation, } from "@/features/base/queries/base-view-query"; -import { activeViewIdAtom } from "@/features/base/atoms/base-atoms"; +import { activeViewIdAtomFamily } from "@/features/base/atoms/base-atoms"; import { useBaseTable } from "@/features/base/hooks/use-base-table"; import { useRowSelection } from "@/features/base/hooks/use-row-selection"; import useCurrentUser from "@/features/user/hooks/use-current-user"; @@ -53,7 +53,7 @@ export function BaseTable({ pageId, embedded }: BaseTableProps) { useBaseSocket(pageId); const { data: base, isLoading: baseLoading, error: baseError } = useBaseQuery(pageId); - const [activeViewId, setActiveViewId] = useAtom(activeViewIdAtom) as unknown as [string | null, (val: string | null) => void]; + const [activeViewId, setActiveViewId] = useAtom(activeViewIdAtomFamily(pageId)) as unknown as [string | null, (val: string | null) => void]; const views = base?.views ?? []; const activeView = useMemo(() => { @@ -146,7 +146,7 @@ export function BaseTable({ pageId, embedded }: BaseTableProps) { } }, [activeView, activeViewId, setActiveViewId]); - const { clear: clearSelection } = useRowSelection(); + const { clear: clearSelection } = useRowSelection(pageId); useEffect(() => { clearSelection(); }, [pageId, activeView?.id, clearSelection]); diff --git a/apps/client/src/features/base/components/grid/grid-cell.tsx b/apps/client/src/features/base/components/grid/grid-cell.tsx index 6f7e11dd4..0a4feca33 100644 --- a/apps/client/src/features/base/components/grid/grid-cell.tsx +++ b/apps/client/src/features/base/components/grid/grid-cell.tsx @@ -2,7 +2,7 @@ import { memo, useCallback } from "react"; import { Cell } from "@tanstack/react-table"; import { useAtom } from "jotai"; import { IBaseRow, IBaseProperty, EditingCell } from "@/features/base/types/base.types"; -import { editingCellAtom } from "@/features/base/atoms/base-atoms"; +import { editingCellAtomFamily } from "@/features/base/atoms/base-atoms"; import { isSystemPropertyType } from "@/features/base/hooks/use-base-table"; import { CellText } from "@/features/base/components/cells/cell-text"; import { CellNumber } from "@/features/base/components/cells/cell-number"; @@ -65,6 +65,7 @@ type GridCellProps = { onCellUpdate: (rowId: string, propertyId: string, value: unknown) => void; rowDragProps?: RowDragProps; orderedRowIds?: string[]; + pageId: string; }; export const GridCell = memo(function GridCell({ @@ -73,13 +74,14 @@ export const GridCell = memo(function GridCell({ onCellUpdate, rowDragProps, orderedRowIds, + pageId, }: GridCellProps) { const property = cell.column.columnDef.meta?.property; const isRowNumber = cell.column.id === "__row_number"; const isPinned = cell.column.getIsPinned(); const pinOffset = isPinned ? cell.column.getStart("left") : undefined; - const [editingCell, setEditingCell] = useAtom(editingCellAtom) as unknown as [EditingCell, (val: EditingCell) => void]; + const [editingCell, setEditingCell] = useAtom(editingCellAtomFamily(pageId)) as unknown as [EditingCell, (val: EditingCell) => void]; const rowId = cell.row.id; const isEditing = @@ -121,6 +123,7 @@ export const GridCell = memo(function GridCell({ isPinned={Boolean(isPinned)} pinOffset={pinOffset} rowDragProps={rowDragProps} + pageId={pageId} /> ); } diff --git a/apps/client/src/features/base/components/grid/grid-container.tsx b/apps/client/src/features/base/components/grid/grid-container.tsx index c3b98c00e..65c7deaf1 100644 --- a/apps/client/src/features/base/components/grid/grid-container.tsx +++ b/apps/client/src/features/base/components/grid/grid-container.tsx @@ -22,7 +22,7 @@ import { } from "@dnd-kit/sortable"; import { restrictToHorizontalAxis } from "@dnd-kit/modifiers"; import { IBaseRow, IBaseProperty, EditingCell } from "@/features/base/types/base.types"; -import { editingCellAtom, activePropertyMenuAtom, propertyMenuDirtyAtom, propertyMenuCloseRequestAtom } from "@/features/base/atoms/base-atoms"; +import { editingCellAtomFamily, activePropertyMenuAtomFamily, propertyMenuDirtyAtomFamily, propertyMenuCloseRequestAtomFamily } from "@/features/base/atoms/base-atoms"; import { useColumnResize } from "@/features/base/hooks/use-column-resize"; import { useGridKeyboardNav } from "@/features/base/hooks/use-grid-keyboard-nav"; import { useRowDrag } from "@/features/base/hooks/use-row-drag"; @@ -53,7 +53,7 @@ type GridContainerProps = { properties: IBaseProperty[]; onCellUpdate: (rowId: string, propertyId: string, value: unknown) => void; onAddRow?: () => void; - pageId?: string; + pageId: string; onColumnReorder?: (columnId: string, overColumnId: string) => void; onResizeEnd?: () => void; onRowReorder?: (rowId: string, targetRowId: string, position: "above" | "below") => void; @@ -96,16 +96,16 @@ export function GridContainer({ const lastTriggeredRowsLenRef = useRef(0); const rows = table.getRowModel().rows; - const [editingCell, setEditingCell] = useAtom(editingCellAtom) as unknown as [EditingCell, (val: EditingCell) => void]; - const [, setActivePropertyMenu] = useAtom(activePropertyMenuAtom) as unknown as [string | null, (val: string | null) => void]; - const [propertyMenuDirty] = useAtom(propertyMenuDirtyAtom) as unknown as [boolean]; - const [, setCloseRequest] = useAtom(propertyMenuCloseRequestAtom) as unknown as [number, (val: number) => void]; + const [editingCell, setEditingCell] = useAtom(editingCellAtomFamily(pageId)) as unknown as [EditingCell, (val: EditingCell) => void]; + const [, setActivePropertyMenu] = useAtom(activePropertyMenuAtomFamily(pageId)) as unknown as [string | null, (val: string | null) => void]; + const [propertyMenuDirty] = useAtom(propertyMenuDirtyAtomFamily(pageId)) as unknown as [boolean]; + const [, setCloseRequest] = useAtom(propertyMenuCloseRequestAtomFamily(pageId)) as unknown as [number, (val: number) => void]; const propertyMenuDirtyRef = useRef(propertyMenuDirty); propertyMenuDirtyRef.current = propertyMenuDirty; const closeRequestCounterRef = useRef(0); - const { selectionCount, clear: clearSelection } = useRowSelection(); - const { deleteSelected } = useDeleteSelectedRows(pageId ?? ""); + const { selectionCount, clear: clearSelection } = useRowSelection(pageId); + const { deleteSelected } = useDeleteSelectedRows(pageId); useEffect(() => { const handleMouseDown = (e: MouseEvent) => { @@ -366,6 +366,7 @@ export function GridContainer({ onCellUpdate={onCellUpdate} orderedRowIds={rowIds} columnVisibility={table.getState().columnVisibility} + pageId={pageId} dragHandlers={ onRowReorder ? { diff --git a/apps/client/src/features/base/components/grid/grid-header-cell.tsx b/apps/client/src/features/base/components/grid/grid-header-cell.tsx index 62bd454b3..d800b1cf5 100644 --- a/apps/client/src/features/base/components/grid/grid-header-cell.tsx +++ b/apps/client/src/features/base/components/grid/grid-header-cell.tsx @@ -5,7 +5,7 @@ import { CSS } from "@dnd-kit/utilities"; import { Popover } from "@mantine/core"; import { useAtom } from "jotai"; import { IBaseRow, IBaseProperty, EditingCell } from "@/features/base/types/base.types"; -import { activePropertyMenuAtom, propertyMenuDirtyAtom, propertyMenuCloseRequestAtom, editingCellAtom } from "@/features/base/atoms/base-atoms"; +import { activePropertyMenuAtomFamily, propertyMenuDirtyAtomFamily, propertyMenuCloseRequestAtomFamily, editingCellAtomFamily } from "@/features/base/atoms/base-atoms"; import { IconLetterT, IconHash, @@ -48,25 +48,27 @@ type GridHeaderCellProps = { header: Header; property: IBaseProperty | undefined; loadedRowIds: string[]; + pageId: string; }; export const GridHeaderCell = memo(function GridHeaderCell({ header, property, loadedRowIds, + pageId, }: GridHeaderCellProps) { const isRowNumber = header.column.id === "__row_number"; const isPinned = header.column.getIsPinned(); const pinOffset = isPinned ? header.column.getStart("left") : undefined; - const { selectionCount } = useRowSelection(); + const { selectionCount } = useRowSelection(pageId); const hasSelection = selectionCount > 0; - const [activePropertyMenu, setActivePropertyMenu] = useAtom(activePropertyMenuAtom) as unknown as [string | null, (val: string | null) => void]; + const [activePropertyMenu, setActivePropertyMenu] = useAtom(activePropertyMenuAtomFamily(pageId)) as unknown as [string | null, (val: string | null) => void]; const menuOpened = activePropertyMenu === header.column.id; const cellRef = useRef(null); - const [propertyMenuDirty, setPropertyMenuDirty] = useAtom(propertyMenuDirtyAtom) as unknown as [boolean, (val: boolean) => void]; - const [closeRequest, setCloseRequest] = useAtom(propertyMenuCloseRequestAtom) as unknown as [number, (val: number) => void]; - const [, setEditingCell] = useAtom(editingCellAtom) as unknown as [EditingCell, (val: EditingCell) => void]; + const [propertyMenuDirty, setPropertyMenuDirty] = useAtom(propertyMenuDirtyAtomFamily(pageId)) as unknown as [boolean, (val: boolean) => void]; + const [closeRequest, setCloseRequest] = useAtom(propertyMenuCloseRequestAtomFamily(pageId)) as unknown as [number, (val: number) => void]; + const [, setEditingCell] = useAtom(editingCellAtomFamily(pageId)) as unknown as [EditingCell, (val: EditingCell) => void]; const handleDirtyChange = useCallback((dirty: boolean) => { setPropertyMenuDirty(dirty); @@ -109,7 +111,7 @@ export const GridHeaderCell = memo(function GridHeaderCell({ // Mantine's built-in `closeOnEscape` only fires when focus is inside the // dropdown, but opening the property menu (clicking the header) leaves // focus on the header itself. Mirror the click-outside path: when dirty, - // bump `propertyMenuCloseRequestAtom` so property-menu shows its + // bump `propertyMenuCloseRequestAtomFamily` so property-menu shows its // "Unsaved changes" confirmation panel; otherwise close directly. useEffect(() => { if (!menuOpened) return; @@ -156,7 +158,7 @@ export const GridHeaderCell = memo(function GridHeaderCell({ {...(isSortableDisabled ? {} : listeners)} > {isRowNumber ? ( - + ) : (
{TypeIcon && ( @@ -207,6 +209,7 @@ export const GridHeaderCell = memo(function GridHeaderCell({ opened={menuOpened} onClose={handleMenuClose} onDirtyChange={handleDirtyChange} + pageId={pageId} /> diff --git a/apps/client/src/features/base/components/grid/grid-header.tsx b/apps/client/src/features/base/components/grid/grid-header.tsx index 6c0585935..aa9c31917 100644 --- a/apps/client/src/features/base/components/grid/grid-header.tsx +++ b/apps/client/src/features/base/components/grid/grid-header.tsx @@ -7,7 +7,7 @@ import classes from "@/features/base/styles/grid.module.css"; type GridHeaderProps = { table: Table; - pageId?: string; + pageId: string; // Passed explicitly to break memo when columns change // (table ref is stable from useReactTable, so memo won't fire without these) columnOrder: ColumnOrderState; @@ -43,15 +43,14 @@ export const GridHeader = memo(function GridHeader({ header={header} property={propertyById.get(header.column.id)} loadedRowIds={loadedRowIds} + pageId={pageId} /> ))} - {pageId && ( - - )} +
); }); diff --git a/apps/client/src/features/base/components/grid/grid-row.tsx b/apps/client/src/features/base/components/grid/grid-row.tsx index 238e3ea58..59a4ea9af 100644 --- a/apps/client/src/features/base/components/grid/grid-row.tsx +++ b/apps/client/src/features/base/components/grid/grid-row.tsx @@ -22,6 +22,7 @@ type GridRowProps = { dragHandlers?: RowDragHandlers; orderedRowIds: string[]; columnVisibility: VisibilityState; + pageId: string; }; export const GridRow = memo(function GridRow({ @@ -32,8 +33,9 @@ export const GridRow = memo(function GridRow({ orderedRowIds, // eslint-disable-next-line @typescript-eslint/no-unused-vars columnVisibility: _columnVisibility, + pageId, }: GridRowProps) { - const isSelected = useRowSelection().isSelected(row.id); + const isSelected = useRowSelection(pageId).isSelected(row.id); const handleDragStart = useCallback( (e: React.DragEvent) => { e.dataTransfer.effectAllowed = "move"; @@ -76,6 +78,7 @@ export const GridRow = memo(function GridRow({ rowIndex={rowIndex} onCellUpdate={onCellUpdate} orderedRowIds={orderedRowIds} + pageId={pageId} rowDragProps={ isRowNumber && dragHandlers ? { diff --git a/apps/client/src/features/base/components/grid/row-number-cell.tsx b/apps/client/src/features/base/components/grid/row-number-cell.tsx index 4671cf90c..9cb393d17 100644 --- a/apps/client/src/features/base/components/grid/row-number-cell.tsx +++ b/apps/client/src/features/base/components/grid/row-number-cell.tsx @@ -16,6 +16,7 @@ type RowNumberCellProps = { isPinned: boolean; pinOffset?: number; rowDragProps?: RowDragProps; + pageId: string; }; export const RowNumberCell = memo(function RowNumberCell({ @@ -25,8 +26,9 @@ export const RowNumberCell = memo(function RowNumberCell({ isPinned, pinOffset, rowDragProps, + pageId, }: RowNumberCellProps) { - const { isSelected, toggle } = useRowSelection(); + const { isSelected, toggle } = useRowSelection(pageId); const selected = isSelected(rowId); const handleCheckboxChange = useCallback( diff --git a/apps/client/src/features/base/components/grid/row-number-header-cell.tsx b/apps/client/src/features/base/components/grid/row-number-header-cell.tsx index 0a0c1a018..405d214d8 100644 --- a/apps/client/src/features/base/components/grid/row-number-header-cell.tsx +++ b/apps/client/src/features/base/components/grid/row-number-header-cell.tsx @@ -5,12 +5,14 @@ import classes from "@/features/base/styles/grid.module.css"; type RowNumberHeaderCellProps = { loadedRowIds: string[]; + pageId: string; }; export const RowNumberHeaderCell = memo(function RowNumberHeaderCell({ loadedRowIds, + pageId, }: RowNumberHeaderCellProps) { - const { selectedIds, toggleAll } = useRowSelection(); + const { selectedIds, toggleAll } = useRowSelection(pageId); const { checked, indeterminate } = useMemo(() => { if (loadedRowIds.length === 0) { diff --git a/apps/client/src/features/base/components/grid/selection-action-bar.tsx b/apps/client/src/features/base/components/grid/selection-action-bar.tsx index adab15301..cec1adc50 100644 --- a/apps/client/src/features/base/components/grid/selection-action-bar.tsx +++ b/apps/client/src/features/base/components/grid/selection-action-bar.tsx @@ -14,7 +14,7 @@ export const SelectionActionBar = memo(function SelectionActionBar({ pageId, }: SelectionActionBarProps) { const { t } = useTranslation(); - const { selectionCount, clear } = useRowSelection(); + const { selectionCount, clear } = useRowSelection(pageId); const { deleteSelected, isPending } = useDeleteSelectedRows(pageId); const isOpen = selectionCount > 0; diff --git a/apps/client/src/features/base/components/property/property-menu.tsx b/apps/client/src/features/base/components/property/property-menu.tsx index 322922326..7b2bb3353 100644 --- a/apps/client/src/features/base/components/property/property-menu.tsx +++ b/apps/client/src/features/base/components/property/property-menu.tsx @@ -18,7 +18,7 @@ import { } from "@tabler/icons-react"; import { IBaseProperty } from "@/features/base/types/base.types"; import { useAtom } from "jotai"; -import { propertyMenuCloseRequestAtom } from "@/features/base/atoms/base-atoms"; +import { propertyMenuCloseRequestAtomFamily } from "@/features/base/atoms/base-atoms"; import { useUpdatePropertyMutation, useDeletePropertyMutation, @@ -34,6 +34,7 @@ type PropertyMenuContentProps = { opened: boolean; onClose: () => void; onDirtyChange?: (dirty: boolean) => void; + pageId: string; }; type MenuPanel = "main" | "rename" | "options" | "confirmDelete" | "confirmDiscard"; @@ -43,6 +44,7 @@ export function PropertyMenuContent({ opened, onClose, onDirtyChange, + pageId, }: PropertyMenuContentProps) { const { t } = useTranslation(); const [panel, setPanel] = useState("main"); @@ -51,7 +53,7 @@ export function PropertyMenuContent({ const [optionsDirty, setOptionsDirty] = useState(false); const pendingActionRef = useRef<"back" | "close" | null>(null); const sourcePanelRef = useRef<"rename" | "options" | null>(null); - const [closeRequest] = useAtom(propertyMenuCloseRequestAtom) as unknown as [number]; + const [closeRequest] = useAtom(propertyMenuCloseRequestAtomFamily(pageId)) as unknown as [number]; const closeRequestRef = useRef(closeRequest); const renameDirty = renameValue !== property.name; @@ -79,7 +81,7 @@ export function PropertyMenuContent({ // Single dirty signal to the outside — reflects whichever panel is // currently accumulating unsaved work. Keeps rename and options in - // lockstep with the `propertyMenuDirtyAtom` so the grid-container's + // lockstep with the `propertyMenuDirtyAtomFamily` so the grid-container's // outside-click handler and the header's ESC handler both prompt // "Unsaved changes" consistently. useEffect(() => { diff --git a/apps/client/src/features/base/hooks/use-base-socket.ts b/apps/client/src/features/base/hooks/use-base-socket.ts index 9c925b1de..41174e017 100644 --- a/apps/client/src/features/base/hooks/use-base-socket.ts +++ b/apps/client/src/features/base/hooks/use-base-socket.ts @@ -7,7 +7,7 @@ import { IBaseRow, IBaseView, } from "@/features/base/types/base.types"; -import { selectedRowIdsAtom } from "@/features/base/atoms/base-atoms"; +import { selectedRowIdsAtomFamily } from "@/features/base/atoms/base-atoms"; import { formulaRecomputeAtom } from "@/features/base/atoms/formula-recompute-atom"; import { IPagination } from "@/lib/types"; @@ -211,11 +211,12 @@ export function useBaseSocket(pageId: string | undefined): void { }, ); const store = getDefaultStore(); - const current = store.get(selectedRowIdsAtom); + const selectedIdsAtom = selectedRowIdsAtomFamily(pageId); + const current = store.get(selectedIdsAtom); if (current.has(e.rowId)) { const next = new Set(current); next.delete(e.rowId); - store.set(selectedRowIdsAtom, next); + store.set(selectedIdsAtom, next); } break; } @@ -236,14 +237,15 @@ export function useBaseSocket(pageId: string | undefined): void { }, ); const store = getDefaultStore(); - const current = store.get(selectedRowIdsAtom); + const selectedIdsAtom = selectedRowIdsAtomFamily(pageId); + const current = store.get(selectedIdsAtom); if (current.size > 0) { let changed = false; const next = new Set(current); for (const id of e.rowIds) { if (next.delete(id)) changed = true; } - if (changed) store.set(selectedRowIdsAtom, next); + if (changed) store.set(selectedIdsAtom, next); } break; } diff --git a/apps/client/src/features/base/hooks/use-delete-selected-rows.tsx b/apps/client/src/features/base/hooks/use-delete-selected-rows.tsx index 87e43286b..06678339b 100644 --- a/apps/client/src/features/base/hooks/use-delete-selected-rows.tsx +++ b/apps/client/src/features/base/hooks/use-delete-selected-rows.tsx @@ -10,7 +10,7 @@ const BATCH_SIZE = 500; export function useDeleteSelectedRows(pageId: string) { const { t } = useTranslation(); - const { selectedIds, clear } = useRowSelection(); + const { selectedIds, clear } = useRowSelection(pageId); const mutation = useDeleteRowsMutation(); const runDelete = useCallback( diff --git a/apps/client/src/features/base/hooks/use-row-selection.ts b/apps/client/src/features/base/hooks/use-row-selection.ts index 5e4ccbed2..52e1d7f8a 100644 --- a/apps/client/src/features/base/hooks/use-row-selection.ts +++ b/apps/client/src/features/base/hooks/use-row-selection.ts @@ -1,8 +1,8 @@ import { useCallback } from "react"; import { useAtom } from "jotai"; import { - selectedRowIdsAtom, - lastToggledRowIndexAtom, + selectedRowIdsAtomFamily, + lastToggledRowIndexAtomFamily, } from "@/features/base/atoms/base-atoms"; type ToggleOpts = { @@ -11,13 +11,15 @@ type ToggleOpts = { orderedRowIds: string[]; }; -export function useRowSelection() { - const [selectedIds, setSelectedIds] = useAtom(selectedRowIdsAtom) as unknown as [ +export function useRowSelection(pageId: string) { + const [selectedIds, setSelectedIds] = useAtom( + selectedRowIdsAtomFamily(pageId), + ) as unknown as [ Set, (val: Set | ((prev: Set) => Set)) => void, ]; const [lastToggledIndex, setLastToggledIndex] = useAtom( - lastToggledRowIndexAtom, + lastToggledRowIndexAtomFamily(pageId), ) as unknown as [number | null, (val: number | null) => void]; const isSelected = useCallback(