Skip to content

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

Description

@kumilingus

What happens

useOnElementsMeasured fires on every change:size, not only when the elements were measured. Any resize the application makes itself wakes every subscriber.

In our CI Pipeline Editor demo the layout sizes its group containers around their content. Collapsing a group produces one measurement callback although nothing was re-measured, and on load three callbacks arrive for two measurement passes. A layout that resizes cells therefore re-enters its own callback, and without a guard it runs twice per burst.

What is documented

packages/joint-react/src/hooks/use-on-elements-measured.ts:33, shipped verbatim in dist/types/index.d.ts:

Fires on the first measurement pass (at least one element has been sized) and again whenever an element is resized.

The resizing there is the outcome of a measurement: the content of an element changed, it was measured again, and it turned out to have a different size. That is the pipeline the hook is named after, and the payload's isInitial separates the first such pass from the later ones. A size the application writes itself is not a measurement, so waking the subscribers for it is a defect, not the documented behaviour.

Why it happens

The store's size listener drops the options of the change (packages/joint-react/src/store/graph-changes.ts):

controller.listenTo(graph, 'change:size', (cell: dia.Cell, newSize: dia.Size) => {
    if (!onElementsSizeChange) return;
    onElementsSizeChange(cell.id, newSize);
});

so GraphStore bumps measureState for application writes as well as for its own measurement writes.

The marker to tell them apart already exists. The ResizeObserver pipeline writes with it (packages/joint-react/src/store/graph-store.ts):

model.set(attributes, { [AUTO_SIZE_OPTION]: true });

and the comment right above it states the intent: "marks writes that originate from the ResizeObserver pipeline so change:size listeners can tell our own writes apart from external ones (controlled-mode sync, direct cell.resize, etc.) and avoid feedback loops." The dev-only warnAutoSizeResize listener already uses it.

Proposal

Pass the options through the change:size listener and bump measureState only for measurement writes.

Nothing is lost by narrowing it. Whoever wants every size change, whatever its origin, already has useGraphEvents({ 'change:size': ... }). useOnElementsMeasured is the only way to hear the measurement pipeline, and today it cannot be heard on its own.

Worth keeping separate: the bookkeeping of which elements have a non-zero size (which isInitial and useAreElementsMeasured rest on) should still follow every size change, including seeded and application-set sizes. Only the counter that wakes the subscribers needs the condition.

Workarounds today

  • Compare a signature of the measured sizes in the callback and return early when nothing changed (what the demo does).
  • Resize the layout-owned cells with { silent: true }, which skips change:size altogether. Only safe where nothing else needs the event.

Version

@joint/react 4.3.5, through @joint/react-plus 4.3.3.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions