Skip to content

Commit 329cbaf

Browse files
aldbrclaude
andcommitted
refactor(dashboard): resolve drag-drop groups inside the state updater
useDashboardDragDrop no longer takes userDashboard or mirrors it in a ref. reorderSections receives the source/destination group titles from the drop event and looks the groups up inside the setUserDashboard functional updater, where React hands it the freshest state. This drops the ref, its sync effect, and the userDashboard parameter, and removes the (practically unreachable) window where a drop could read a stale dashboard between a state commit and the ref update. The destination index is copied into a local so the updater stays pure under StrictMode. Draft addressing review comment C6.1 — kept as its own commit for review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c63e3e5 commit 329cbaf

2 files changed

Lines changed: 51 additions & 74 deletions

File tree

packages/diracx-web-components/src/components/DashboardLayout/DashboardDrawer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export default function DashboardDrawer({
7575
const theme = useTheme();
7676

7777
// Set up drag and drop
78-
useDashboardDragDrop(userDashboard, setUserDashboard);
78+
useDashboardDragDrop(setUserDashboard);
7979

8080
const handleAppCreation = (appType: string) => {
8181
const group =
Lines changed: 50 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useEffect, useRef } from "react";
3+
import { useEffect } from "react";
44
import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
55
import { extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
66
import { DashboardGroup } from "../../types";
@@ -10,16 +10,8 @@ import { DashboardGroup } from "../../types";
1010
* Handles reordering items within and between groups.
1111
*/
1212
export default function useDashboardDragDrop(
13-
userDashboard: DashboardGroup[],
1413
setUserDashboard: React.Dispatch<React.SetStateAction<DashboardGroup[]>>,
1514
) {
16-
// Keep a ref to the latest dashboard so the monitor effect does not need
17-
// to re-subscribe every time the dashboard changes.
18-
const userDashboardRef = useRef(userDashboard);
19-
useEffect(() => {
20-
userDashboardRef.current = userDashboard;
21-
}, [userDashboard]);
22-
2315
useEffect(() => {
2416
return monitorForElements({
2517
onDrop({ source, location }) {
@@ -29,100 +21,85 @@ export default function useDashboardDragDrop(
2921
}
3022
const sourceData = source.data;
3123
const targetData = target.data;
32-
const currentDashboard = userDashboardRef.current;
24+
const sourceTitle = sourceData.title as string;
25+
const destinationTitle = targetData.title as string;
26+
const sourceIndex = sourceData.index as number;
3327

3428
if (location.current.dropTargets.length === 2) {
35-
const groupTitle = targetData.title;
3629
const closestEdgeOfTarget = extractClosestEdge(targetData);
3730
const targetIndex = targetData.index as number;
38-
const sourceGroup = currentDashboard.find(
39-
(group) => group.title === sourceData.title,
40-
);
41-
const targetGroup = currentDashboard.find(
42-
(group) => group.title === groupTitle,
43-
);
44-
const sourceIndex = sourceData.index as number;
4531
const destinationIndex = (
4632
closestEdgeOfTarget === "top" ? targetIndex : targetIndex + 1
4733
) as number;
4834

4935
reorderSections(
50-
sourceGroup,
51-
targetGroup,
36+
sourceTitle,
37+
destinationTitle,
5238
sourceIndex,
5339
destinationIndex,
5440
);
5541
} else {
56-
const groupTitle = targetData.title;
57-
const sourceGroup = currentDashboard.find(
58-
(group) => group.title === sourceData.title,
59-
);
60-
const targetGroup = currentDashboard.find(
61-
(group) => group.title === groupTitle,
62-
);
63-
const sourceIndex = sourceData.index as number;
64-
65-
reorderSections(sourceGroup, targetGroup, sourceIndex);
42+
reorderSections(sourceTitle, destinationTitle, sourceIndex);
6643
}
6744
},
6845
});
6946

47+
// The groups are looked up inside the functional updater, so it always
48+
// sees the freshest state — no ref mirror, no re-subscription on every
49+
// dashboard change, and no window where a drop reads a stale dashboard.
7050
function reorderSections(
71-
sourceGroup: DashboardGroup | undefined,
72-
destinationGroup: DashboardGroup | undefined,
51+
sourceTitle: string,
52+
destinationTitle: string,
7353
sourceIndex: number,
7454
destinationIndex: number | null = null,
7555
) {
76-
if (sourceGroup && destinationGroup) {
77-
if (
78-
sourceGroup.title === destinationGroup.title &&
79-
destinationIndex &&
80-
sourceIndex < destinationIndex
81-
) {
82-
destinationIndex -= 1;
56+
setUserDashboard((groups) => {
57+
const sourceGroup = groups.find((group) => group.title === sourceTitle);
58+
const destinationGroup = groups.find(
59+
(group) => group.title === destinationTitle,
60+
);
61+
if (!sourceGroup || !destinationGroup) return groups;
62+
63+
const sameGroup = sourceGroup.title === destinationGroup.title;
64+
// Copy into a local so the updater stays pure (React StrictMode may
65+
// invoke it twice in development).
66+
let destIndex = destinationIndex;
67+
if (sameGroup && destIndex && sourceIndex < destIndex) {
68+
destIndex -= 1;
8369
}
84-
if (
85-
sourceGroup.title === destinationGroup.title &&
86-
(destinationIndex === null || sourceIndex === destinationIndex)
87-
) {
88-
return;
70+
if (sameGroup && (destIndex === null || sourceIndex === destIndex)) {
71+
return groups; // Nothing to do
8972
}
9073

91-
if (sourceGroup.title === destinationGroup.title) {
74+
if (sameGroup) {
9275
const sourceItems = [...sourceGroup.items];
9376
const [removed] = sourceItems.splice(sourceIndex, 1);
94-
if (destinationIndex === null) {
95-
destinationIndex = sourceItems.length;
77+
if (destIndex === null) {
78+
destIndex = sourceItems.length;
9679
}
97-
sourceItems.splice(destinationIndex, 0, removed);
98-
99-
setUserDashboard((groups) =>
100-
groups.map((group) =>
101-
group.title === sourceGroup.title
102-
? { ...group, items: sourceItems }
103-
: group,
104-
),
80+
sourceItems.splice(destIndex, 0, removed);
81+
return groups.map((group) =>
82+
group.title === sourceGroup.title
83+
? { ...group, items: sourceItems }
84+
: group,
10585
);
106-
} else {
107-
const sourceItems = [...sourceGroup.items];
108-
const [removed] = sourceItems.splice(sourceIndex, 1);
109-
const destinationItems = [...destinationGroup.items];
110-
if (destinationIndex === null) {
111-
destinationIndex = destinationItems.length;
112-
}
113-
destinationItems.splice(destinationIndex, 0, removed);
86+
}
11487

115-
setUserDashboard((groups) =>
116-
groups.map((group) =>
117-
group.title === sourceGroup.title
118-
? { ...group, items: sourceItems }
119-
: group.title === destinationGroup.title
120-
? { ...group, items: destinationItems }
121-
: group,
122-
),
123-
);
88+
const sourceItems = [...sourceGroup.items];
89+
const [removed] = sourceItems.splice(sourceIndex, 1);
90+
const destinationItems = [...destinationGroup.items];
91+
if (destIndex === null) {
92+
destIndex = destinationItems.length;
12493
}
125-
}
94+
destinationItems.splice(destIndex, 0, removed);
95+
return groups.map((group) =>
96+
group.title === sourceGroup.title
97+
? { ...group, items: sourceItems }
98+
: group.title === destinationGroup.title
99+
? { ...group, items: destinationItems }
100+
: group,
101+
);
102+
});
126103
}
127104
}, [setUserDashboard]);
128105
}

0 commit comments

Comments
 (0)