-
Notifications
You must be signed in to change notification settings - Fork 75
Description
Problem
The current selection architecture has a bidirectional sync problem between Cytoscape and React:
-
React drives Cytoscape selection via props — selectedNodesIds and selectedEdgesIds arrays are passed into , and useEntitySelection runs a useEffect to imperatively select/unselect Cytoscape elements to match those props.
-
Cytoscape drives React selection via callbacks — When the user clicks in Cytoscape, select/unselect events fire, which call onSelectedElementIdsChange, which calls replaceGraphSelection, which updates Jotai atoms (nodesSelectedIdsAtom, edgesSelectedIdsAtom), which re-renders, which passes new props back into , which triggers useEntitySelection again.
This creates a circular data flow: Cytoscape → callbacks → Jotai atoms → props → useEntitySelection → Cytoscape. The debounced event handlers and change-detection refs in useManageElementsSelection are band-aids to prevent infinite loops.
Additionally, onSelectedElementIdsChange provides separate nodeIds/edgeIds/groupIds sets, so selection order is lost — there's no way to know which element was selected first.
Proposed Changes
1. Make Cytoscape the single source of truth for selection state
- Remove selectedNodesIds, selectedEdgesIds, and selectedGroupsIds from GraphProps / Selection type.
- Remove useEntitySelection (the hook that pushes React state into Cytoscape).
- Remove the Jotai atoms nodesSelectedIdsAtom and edgesSelectedIdsAtom as the authoritative selection state.
2. Read selection state from Cytoscape using useSyncExternalStore
- Subscribe to Cytoscape's select/unselect events as the external store.
- Expose a single ordered list of selected element IDs (preserving selection order) via a hook, likely integrated into useGraphSelection.
- This replaces the current pattern of debounced event handlers writing back to Jotai atoms.
3. Set selection imperatively through graphRef, not through state
- Add a setSelection method to GraphRef (e.g., graphRef.current.setSelection({ vertices, edges })).
- This directly calls cy.getElementById(id).select() / .unselect() — no round-trip through React state.
- Expose this through GraphProvider / useGraphRef so any React code can imperatively set selection.
4. Keep useGraphSelection as the public API
- useGraphSelection continues to be the hook consumers use (GraphViewer, ContextMenu, NodesTabular, EdgesTabular, etc.).
- Internally, it switches from reading Jotai atoms to reading the useSyncExternalStore-backed Cytoscape selection.
- replaceGraphSelection switches from writing Jotai atoms to calling the imperative graphRef.current.setSelection(...).
- Consumer code in ~6 files stays largely unchanged.
5. Remove the onSelectedElementIdsChange callback pattern
- The separate onSelectedNodesIdsChange, onSelectedEdgesIdsChange, onSelectedGroupsIdsChange, and onSelectedElementIdsChange callbacks on GraphProps become unnecessary since React reads selection directly from Cytoscape.
- useManageElementsSelection can be significantly simplified or removed — it no longer needs to sync in either direction.
Files Primarily Affected
| File | Change |
|---|---|
| components/Graph/Graph.model.ts | Remove Selection type, SelectedElements |
| components/Graph/Graph.tsx / GraphProps | Remove selection props and callbacks |
| components/Graph/hooks/useManageElementsSelection.ts | Remove or heavily simplify |
| components/Graph/GraphContext.tsx | Expand GraphRef with setSelection |
| modules/GraphViewer/useGraphSelection.ts | Rewrite internals to use useSyncExternalStore + graphRef |
| modules/GraphViewer/GraphViewer.tsx | Remove selection prop wiring to |
| core/StateProvider/nodes.ts / edges.ts | Remove nodesSelectedIdsAtom / edgesSelectedIdsAtom |
| Consumers of the atoms directly (displayVertex.ts, displayEdge.ts, EntitiesRefreshButton.tsx, NodeExpand.tsx, useRemoveFromGraph.ts, useResetState.ts) | Switch to useGraphSelection or read from the new source |