fix(joint-react): StrictMode duplicate feature registration and resetCells ghost cells under React 18#3420
Merged
Conversation
kumilingus
approved these changes
Jul 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The library supports React 18+, but development and CI ran only on React 19, which hid two React 18 bugs. This PR fixes both and adds a React 18 test run so they stay fixed.
Fixes
Duplicate paper feature under React 18 StrictMode (selection "2x drag" bug).
useCreateFeatureregisters paper-target features synchronously during render. React 18's StrictMode invokes the render body twice, and ref writes from the discarded first pass are not carried into the second — sofeatureRef.currentalone could not dedupe, and a second feature instance was constructed. The duplicate's constructor bound paper listeners that were never cleaned up (only the committed instance'sclean()runs), so every interaction fired twice — e.g. a selection drag translated each cell by 2x. The hook now adopts an instance already registered in the store (external state, which StrictMode does preserve) instead of constructing a duplicate.Ghost cells after
resetCells().graph.resetCells()emits a bulkresetcontaining onlyadds for the surviving cells — no per-cellremoveevents. The reactive container therefore kept cells the reset had dropped, anduseCells()reported ghost cells the canvas no longer showed.graph-changesnow flags reset batches (isReset), andgraph-projectionprunes any container cell not present in the reset's surviving set, mirroring the reconciliation the React-drivenupdateGraph()path already does.Testing
jest.react18.config.mjsruns the full suite with React resolved to an aliased React 18 install (react18/react18-domdev deps); the React-19-specificreact-compilerproject is skipped.yarn testnow runs bothtest:react19(default) andtest:react18.use-reset-cells-ghost.test.tsxcovering both theuseCellscount and the projection container afterresetCells.