Skip to content

fix(joint-react): deliver useOnElementsMeasured once per settled change - #3518

Merged
kumilingus merged 5 commits into
clientIO:devfrom
samuelgja:fix/use-element-measured
Oct 1, 2026
Merged

kumilingus merged 5 commits into
clientIO:devfrom
samuelgja:fix/use-element-measured

Conversation

@samuelgja

@samuelgja samuelgja commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Description

useOnElementsMeasured now delivers one event per settled change, implementing the specification written as tests in #3520. Its test file is in this PR (all 16 cases pass), plus a sibling file with a culled element, removal while waiting, and a content change that re-measures.

An element is outstanding from the moment it enters the graph until it is accounted for by a positive signal, never by its size value (a zero size is a legal final size, e.g. a layout anchor):

  • its React content committed without registering a measurer — markElementRendered(), called from the element portal item's existing layout effect, after the subtree's useMeasureElement has had its chance to register (O(1));
  • no paper renders it (viewport culling, cellVisibility, a hidden group) — settleUnrenderedElements(), run on each paper's render:done, once the views of that pass are in the DOM; O(outstanding), which is empty between changes;
  • it registered a measurer (useMeasureElement / HTMLHost): then until the observer measures it — an autoSize write, or a measurement equal to the size it already holds (the observer writes nothing then and reports it through the new onElementMeasured option);
  • it leaves the graph.

While anything is outstanding no event is delivered: a batch mixing plain and measured elements is one event once the last one is measured. Sizes the application writes (cell.resize(), controlled cells sync) are not measurements and never fire, whether the element is settled or still waiting (#3514). Without any paper nothing renders or measures, so nothing is waited for (store-only use keeps the old timing).

A graph reset starts a new measurement history: measureState returns to 0 until the new diagram settles, and isInitial is reported again for its first pass (measureGeneration).

packages/joint-react/src/store

  • graph-changes.ts: MeasurementListeners (onElementsSizeChange, onElementRemove, onReset), forwarded by graph-projection.ts.
  • graph-store.ts: the bookkeeping above; markElementRendered(), settleUnrenderedElements(), measureGeneration.
  • paper-store.ts: subscribes settleUnrenderedElements to the paper's render:done.
  • create-elements-size-observer.ts: onElementMeasured option.

packages/joint-react/src

  • hooks/use-on-elements-measured.ts: isInitial per generation; the JSDoc states the contract.
  • components/paper/render-element/paper-element-item.tsx: calls markElementRendered() from the existing layout effect (no extra effect per element).

Tests

  • use-on-elements-measured-events.test.tsx (from test(joint-react): specify how many events useOnElementsMeasured delivers #3520, 16 cases). Two adjustments to the file, both in test scaffolding:
    • flush() now also awaits one requestAnimationFrame. The paper is async, so an added element's view mounts, and its useMeasureElement registers, only in the next frame; a plain setTimeout(0) lands before that, while the portal does not exist yet (measured in jsdom: observer.has(id) is false after setTimeout(0) and true after a frame). The helper's own comment describes waiting for that commit, so it now does.
    • In "sizes an element that is waiting", the application write is 70×70 rather than the measured 120×40: writing the measured size itself is a no-op set() in JointJS with no change:size for anything to observe. The equal-size measurement is covered separately, through the observer.
  • use-on-elements-measured-settling.test.tsx (same harness, beside the spec): a culled zero-sized element (seed and batch), a waiting element removed before measurement, and, through a real ResizeObserver mock, a content change that measures larger (one event, model resized), one that measures the same (no event), and an element the application pre-sized to what it then measures (delivered).
  • graph-changes.test.ts, graph-store.test.ts: forwarding and the equal-size measurement; the fromMeasure flag in older tests replaced by autoSize.
  • yarn test in packages/joint-react passes: typecheck, knip, duplicity, React 19 (1096 tests) and React 18 (1092 tests). yarn lint clean. yarn bench passes.

Changeset (@joint/react patch):

  • useOnElementsMeasured - fix to fire exactly once per settled change, when no element is still waiting to be measured

Motivation and Context

Fixes #3514.

Implements and supersedes #3520 (the tests-only spec PR), including its "element that stays zero-sized" cases: an earlier revision of this PR inferred "waiting" from a zero size and held every later batch open for a permanently zero-sized element that no paper renders (the GroupEndModel markers in the ci-pipeline demo). That inference is gone.

Related: #3485 (auto-size re-measurement after external size writes, against master). Both touch graph-store.ts, so expect a small conflict when #3485 reaches dev.

Notes

  • Targets dev, which has no CI; the full suite was run locally (above).
  • Known ceiling: a paper that stays frozen never reaches render:done, so elements added meanwhile are delivered once it renders.

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.

Copilot review overview

🟢 Approval recommended

Only minor documentation and changeset wording nits remain; no blocking issues were identified.

Review effort: Lite
Findings: 1 Low severity

Open (1)
What changed in this PR

Updates @joint/react so useOnElementsMeasured ignores later application-driven resizes while preserving initial size tracking.

Changes:

  • Forwards change:size options through graph listeners.
  • Filters measurement notifications by resize origin.
  • Adds regression tests, documentation, and a patch changeset.
File Summary
packages/​joint-react/​src/​store/​graph-store.ts Filters measurement-state updates.
packages/​joint-react/​src/​store/​graph-projection.ts Extends size-change callback options.
packages/​joint-react/​src/​store/​graph-changes.ts Forwards size-change options.
packages/​joint-react/​src/​store/​__tests__/​graph-changes.test.ts Tests option forwarding.
packages/​joint-react/​src/​hooks/​use-on-elements-measured.ts Documents updated hook behavior.
packages/​joint-react/​src/​hooks/​__tests__/​use-on-elements-measured.test.tsx Adds resize behavior regression tests.
.changeset/​react-elements-measured-app-resize.md Adds the patch changeset.

💡 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/hooks/use-on-elements-measured.ts Outdated
kumilingus added a commit to kumilingus/joint that referenced this pull request Sep 24, 2026
Whether a size written by the application deserves an event depends on
what it does to the graph, not on who wrote it.

Resizing an element that already has a size changes nothing about
readiness, so it must stay silent: that is the re-entrant layout of
clientIO#3514. Sizing an element that had none is the write that makes the graph
settled, so it is the event a consumer is waiting for.

Pinning both keeps a fix from satisfying one by breaking the other.
Suppressing every application resize, as clientIO#3518 does, silences the second
one and leaves the graph fully sized with nobody told.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@kumilingus kumilingus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Update the useOnElementsMeasured using this specs #3520 as a single fix.

- Introduced a new measurement generation system in GraphStore to track resets and initial measurement passes.
- Updated `useOnElementsMeasured` to deliver events based on settled changes, ensuring accurate measurement reporting.
- Modified the measurement observer to handle cases where elements are added without sizes and to trigger events when they are measured.
- Improved test coverage for measurement events, including scenarios for element resets and size changes.
- Refactored size change handling to differentiate between application-resized elements and those measured by the observer.
- Ensured that the `markElementRendered` function correctly settles elements that arrive without sizes.
@samuelgja samuelgja changed the title fix(joint-react): fire useOnElementsMeasured only for measurement writes fix(joint-react): deliver useOnElementsMeasured once per settled change Sep 29, 2026
…ed elements

- Updated the measurement handling logic in useOnElementsMeasured to account for elements that are not rendered by the paper or are zero-sized.
- Introduced new tests to cover scenarios where elements are culled or removed before measurement.
- Improved the flush function to ensure all rendering and measurement processes are settled before events are delivered.
- Refactored GraphStore to manage outstanding elements more effectively, ensuring that elements not rendered by any paper do not block measurement events.
- Added a settleUnrenderedElements method to clear outstanding elements that are not accounted for by any paper.
- Enhanced documentation to clarify the behavior of the measurement system regarding application-written sizes and unmeasured elements.
@kumilingus
kumilingus merged commit e98ee88 into clientIO:dev Oct 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useOnElementsMeasured fires on every change:size, not only on a measurement pass

3 participants