anvil: stale closure in click handler captures drag state at mount time - canvas clicks not suppressed during active widget drag

Open Beginner friendly
#42,118 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
react, typescript
Domain
frontend

Research direction

Start in app/client/src/layoutSystems/anvil/editor/canvas/AnvilEditorCanvas.tsx and trace handleOnClickCapture into hooks/useClickToClearSelections.ts. Reproduce a widget drag followed by a canvas click, then verify the registered handler uses current drag, resize, and space-distribution state and does not trigger selection or property-pane actions during the drag.

Written by the indexing model from the issue text.

Description

Bug Description

AnvilEditorCanvas registers a click handler via ddEventListener in a useEffect with an empty dependency array ([]). The handler closes over drag/resize state values from Redux. Because the effect never re-runs, the handler always uses the state captured at mount time. When drag state changes, the click guard reads stale values and fails to suppress canvas clicks during active drag operations.

Affected files

pp/client/src/layoutSystems/anvil/editor/canvas/AnvilEditorCanvas.tsx, lines 47-53:

ypescript useEffect(() => { canvasRef.current?.addEventListener("click", handleOnClickCapture); return () => { canvasRef.current?.removeEventListener("click", handleOnClickCapture); }; }, []); // empty deps - handler never updated

pp/client/src/layoutSystems/anvil/editor/canvas/hooks/useClickToClearSelections.ts, lines 18-31:

` ypescript
const isDragging = useSelector(state => state.ui.widgetDragResize.isDragging);
const isCanvasResizing = useSelector(state => state.ui.widgetDragResize.isAutoCanvasResizing);
const isDistributingSpace = useSelector(getAnvilSpaceDistributionStatus);

return (e) => {
if (!(isDragging || isCanvasResizing || isDistributingSpace)) { // stale values
goToWidgetAdd();
focusWidget(widgetId);
showPropertyPane();
e.preventDefault();
}
};
`

Failure scenario

  1. User opens the Anvil editor. At mount, isDragging = false.
  2. User starts dragging a widget. Redux updates isDragging = true.
  3. useClickToClearSelections returns a new function with the updated value.
  4. useCallback creates a new handleOnClickCapture reference.
  5. The useEffect with [] deps does NOT re-run - the old handler (with isDragging = false) remains registered.
  6. User clicks the canvas during the drag.
  7. The stale handler sees isDragging = false, passes the guard, and calls goToWidgetAdd(), ocusWidget(), and showPropertyPane().
  8. Widget selection is disrupted mid-drag. The property pane jumps to a different widget. The drag may be cancelled unexpectedly.

Fix

Add handleOnClickCapture to the dependency array, or use a ref to hold the latest handler:

` ypescript
const handleOnClickCaptureRef = useRef(handleOnClickCapture);
useEffect(() => { handleOnClickCaptureRef.current = handleOnClickCapture; });

useEffect(() => {
const handler = (e: MouseEvent) => handleOnClickCaptureRef.current(e);
canvasRef.current?.addEventListener("click", handler);
return () => { canvasRef.current?.removeEventListener("click", handler); };
}, []);
`

Environment

Appsmith elease branch (2026-08-13), React 18.

Dominant language
TypeScript
Stars
40.9k
Forks
4.8k
Avg merge
1d 12h
Merged PRs (30d)
47

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from appsmithorg/appsmith

All issues in appsmithorg/appsmith

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.