fix: stale closure bug preventing canvas tools from creating elements

- CanvasArea: add useEffect to refresh InteractionEngine callbacks when props change
- App.tsx: handleElementCreated/Deleted/Modified now use functional setElements updates
- Root cause: CanvasArea init useEffect had empty deps, capturing stale callbacks at mount
- handleElementCreated used [...elements, el] with stale elements=[] closure
- Each new element replaced all previous ones instead of appending
- All 343 tests pass, tsc clean
This commit is contained in:
2026-06-26 18:57:09 +02:00
parent 3bec3938d0
commit 7e530dd09a
2 changed files with 42 additions and 19 deletions
+14 -8
View File
@@ -293,11 +293,13 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
}, [collab.elements, collab.layers, collab.blocks, collab.status]);
const handleElementCreated = useCallback((el: CADElement) => {
const newElements = [...elements, el];
setElements((prev) => {
const newElements = [...prev, el];
historyManagerRef.current.pushSnapshot({
elements: newElements, layers, blocks, groups, bgConfig,
}, 'Element erstellt');
setElements(newElements);
return newElements;
});
syncHistory();
// Push to Yjs CRDT for real-time sync
collab.setElement(el);
@@ -311,14 +313,16 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
setSavedStatus('Fehler beim Speichern');
});
}
}, [elements, layers, blocks, groups, bgConfig, syncHistory, drawingId, token, collab]);
}, [layers, blocks, groups, bgConfig, syncHistory, drawingId, token, collab]);
const handleElementsDeleted = useCallback((ids: string[]) => {
const newElements = elements.filter((e) => !ids.includes(e.id));
setElements((prev) => {
const newElements = prev.filter((e) => !ids.includes(e.id));
historyManagerRef.current.pushSnapshot({
elements: newElements, layers, blocks, groups, bgConfig,
}, `${ids.length} Element(e) gelöscht`);
setElements(newElements);
return newElements;
});
syncHistory();
// Push deletions to Yjs CRDT for real-time sync
ids.forEach(id => collab.deleteElement(id));
@@ -335,14 +339,16 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
}, [elements, layers, blocks, groups, bgConfig, syncHistory, token, collab]);
const handleElementsModified = useCallback((modified: CADElement[]) => {
const newElements = elements.map((e) => {
setElements((prev) => {
const newElements = prev.map((e) => {
const found = modified.find((m) => m.id === e.id);
return found ?? e;
});
historyManagerRef.current.pushSnapshot({
elements: newElements, layers, blocks, groups, bgConfig,
}, `${modified.length} Element(e) geändert`);
setElements(newElements);
return newElements;
});
syncHistory();
// Push modifications to Yjs CRDT for real-time sync
modified.forEach(el => collab.setElement(el));
@@ -356,7 +362,7 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
setSavedStatus('Fehler beim Speichern');
});
}
}, [elements, layers, blocks, groups, bgConfig, syncHistory, token, collab]);
}, [layers, blocks, groups, bgConfig, syncHistory, token, collab]);
const lastCursorUpdateRef = useRef(0);
const handleCursorMoved = useCallback((x: number, y: number) => {
+17
View File
@@ -72,6 +72,23 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Update callbacks when they change (avoids stale closures)
useEffect(() => {
const interaction = interactionRef.current;
if (!interaction) return;
interaction.setCallbacks({
onElementCreated,
onElementsDeleted,
onElementsModified,
onCursorMoved,
onToolStateChanged,
onTextEdit,
onCommandTrigger,
onSelectionChange,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onElementCreated, onElementsDeleted, onElementsModified, onCursorMoved, onToolStateChanged, onTextEdit, onCommandTrigger, onSelectionChange]);
// Resize canvas to fill container
useEffect(() => {
const canvas = canvasRef.current;