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.
This commit is contained in:
Philipinho
2026-04-27 21:35:39 +01:00
parent cd8d1e0ed8
commit bfa85b9835
14 changed files with 96 additions and 54 deletions
@@ -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;
}
@@ -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(
@@ -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<string>,
(val: Set<string> | ((prev: Set<string>) => Set<string>)) => void,
];
const [lastToggledIndex, setLastToggledIndex] = useAtom(
lastToggledRowIndexAtom,
lastToggledRowIndexAtomFamily(pageId),
) as unknown as [number | null, (val: number | null) => void];
const isSelected = useCallback(