Skip to content

fix(joint-react): prevent stale layout:update entries from resurrecting removed cells - #3494

Open
samuelgja wants to merge 4 commits into
clientIO:masterfrom
samuelgja:fix/react-external-graph-undo
Open

fix(joint-react): prevent stale layout:update entries from resurrecting removed cells#3494
samuelgja wants to merge 4 commits into
clientIO:masterfrom
samuelgja:fix/react-external-graph-undo

Conversation

@samuelgja

Copy link
Copy Markdown
Contributor

Description

Fixes a permanent render loss with externally-owned graphs (<GraphProvider graph={graph}> +
imperative mutations + CommandManager undo): after deleting an element that has a link and
undoing, the restored element came back positioned and sized but empty — renderElement was
never called for it again — and the restored link stayed visibility: hidden.

Mechanism: layout:update change batches hold direct cell references and are produced
asynchronously (paper view-mount re-broadcasts via setPaperViews, app layout pipelines
applying async results such as ELK). When a removal landed between producing and processing
such a batch, the projection wrote the entry into the cells container without checking that
the cell was still in the graph — resurrecting the removed cell's record. From then on the
container permanently disagreed with the graph: the undo's re-add of the byte-identical cell
merged into the stale record (mergeCellRecord identity fast path → isStrictEqual early
return), so no version bump and no membership notification ever fired, useContainerKeys
kept its cached ids, and the cell never rendered again.

Changes in graph-projection.ts:

  • add/change entries are gated on graph membership; a stale entry now also repairs the
    container by deleting a lingering record.
  • remove entries only apply when the cell has actually left the graph, so a paper
    view-unmount notification (which can name a still-mounted cell, e.g. viewport culling)
    can no longer delete a live cell's record.
  • On element removal, the container is swept once per batch for orphaned link records
    (graph.getConnectedLinks cannot name them at that point — the cells are already out of
    the graph adjacency). The sweep skips links the graph still holds, so links re-added
    within the same batch survive.

Change in use-create-portal-paper.tsx:

  • Links parked hidden in pendingLinks (endpoint's React content not painted yet) were only
    rechecked from insertView and joint's afterRender; portal content mounting in a React
    commit triggers neither, so a link could stay hidden permanently once its endpoint painted.
    A useEffect now rechecks after every commit that (re)rendered the element portals. The
    effect is O(1) in steady state (pendingLinks.size === 0 early return) and its dependency
    does not change on cell data/position updates, so it never runs on drag frames.

Tests: new external-graph-undo.test.tsx covers the delete+undo scenario end to end
(projection pruning, stale-entry resurrection, live-cell view-unmount safety, undo repaint
with link visibility). The graph-projection-edge-cases sweep test was updated to exercise
the sweep through a genuinely missed link removal instead of a synthetic remove entry for a
cell still in the graph (which is exactly the unsafe input this fix rejects).

Motivation and Context

Reported against 4.3.5 by a customer running an externally-owned dia.Graph subclass with
imperative mutations (fromJSON, stencil drops, layout) and CommandManager undo/redo:
delete an element with a link, press Ctrl+Z, and the shape returns empty with its link
hidden, unrecoverably. Their measurement (graph at 2 cells while the projection held 4,
including the deleted element and its link) matches the resurrection mechanism above.

Screenshots (if appropriate):

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Delayed updates can still overwrite replacement cells sharing the same ID, and the changeset violates repository formatting requirements.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Fixes stale graph projection records that prevent restored cells and links from rendering after undo.

Changes:

  • Validates projection updates against graph membership and prunes orphaned links.
  • Rechecks hidden pending links after portal commits.
  • Adds regression coverage for external-graph delete/undo behavior.
File summaries
File Description
graph-projection.ts Reconciles stale cell and link records.
graph-projection-edge-cases.test.ts Updates orphan-link sweep coverage.
use-create-portal-paper.tsx Rechecks pending links after React commits.
external-graph-undo.test.tsx Adds end-to-end undo regression tests.
.changeset/eleven-moments-heal.md Documents the patch release.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/joint-react/src/store/graph-projection.ts Outdated
Comment thread .changeset/eleven-moments-heal.md Outdated
@samuelgja
samuelgja marked this pull request as ready for review September 3, 2026 05:55
@samuelgja
samuelgja requested review from kumilingus and a balanced review from Copilot September 3, 2026 05:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Two moderate correctness issues can leave links hidden or emit stale removal deltas.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread packages/joint-react/src/hooks/use-create-portal-paper.tsx Outdated
Comment thread packages/joint-react/src/store/graph-projection.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The pending-link observer and quadratic scanning issues must be addressed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Balanced

}
}

if (this.pendingLinks.size === 0) this.disconnectPortalObserver();
if (!isSourceReady || !isTargetReady) {
view.el.style.visibility = 'hidden';
this.pendingLinks.add(cellId);
this.observePendingLinkEndpoints();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants