Skip to content

feat(joint-react): add useOnCellsChange and measurement selectors - #3532

Open
samuelgja wants to merge 9 commits into
clientIO:devfrom
samuelgja:feat/new-react-measure-module
Open

samuelgja wants to merge 9 commits into
clientIO:devfrom
samuelgja:feat/new-react-measure-module

Conversation

@samuelgja

Copy link
Copy Markdown
Contributor

Description

Measuring was spread over graph-store.ts, graph-changes.ts, graph-projection.ts and create-elements-size-observer.ts, and the only way to react to it was useOnElementsMeasured. This PR moves all of it into one module and exposes the measured state as selectors, so it is consumed with the hooks that already exist (useCells) plus one new effect-like hook (useOnCellsChange). Nothing is removed from the public API; useOnElementsMeasured stays and is marked @deprecated.

src/store/measurement.ts (new, replaces create-elements-size-observer.ts)

  • createMeasurement({ graph, autoSizeOrigin, paperStores }), exposed as graphStore.measurement. It owns its own mvc.Listener on the graph (add / remove / reset / change:size), the node registry and the ResizeObserver.
  • The ResizeObserver is created by the first observe(): a diagram that never measures does not have one.
  • graph-store.ts, graph-changes.ts, graph-projection.ts and state-container.ts no longer contain measurement code. use-are-elements-measured.ts and two dead helpers in clear-view.ts are deleted.
  • Behaviour kept from fix(joint-react): deliver useOnElementsMeasured once per settled change #3518: one change per settled pass, application-written sizes are not measurements, a zero size is a legal final size.
  • Behaviour changes: an element removal is a settled change; a diagram with no elements is "not measured"; elements already in an external graph are tracked from the start; an element whose measured node unmounts no longer blocks later events.

src/selectors/ (reorganised)

  • measurement-selectors.ts: selectMeasuredState (0 while unmeasured, otherwise a value that changes once per settled change and is never reused), selectIsMeasured, selectElementsSizes (ReadonlyMap<CellId, dia.Size>, same reference until a size / add / remove / reset).
  • source-selector.ts: createSourceSelector. These selectors read a store source, not the cells, so useCells / useOnCellsChange subscribe to that source only: no work on other commits (a drag frame builds no cells array - asserted by a test). They must be used in the all-cells form; with an id, ids or a collection the hook throws.
  • store-selectors.ts (internal snapshot selectors) and selector-utils.ts (equality helpers, moved from src/utils). cell-selectors.ts is unchanged.

src/hooks

  • useOnCellsChange(selector, onChange, isEqual?) and the id / ids / collection forms: useCells as an effect. The callback runs once on mount and then synchronously with each store change that changes the selected value, without re-rendering.
  • useCells is now a thin wrapper over the internal useCellsSource, shared with useOnCellsChange. isEqual is read through a ref, so an inline isEqual no longer re-subscribes on every render.
  • useOnElementsMeasured: @deprecated, same contract, now reads measurement.stateSource.
  • useMeasureElement: unchanged API; uses measurement.observe and clears the view cache of its own element when its size changes (replaces the global sweep).

Store callbacks (src/utils/scheduler.ts, src/store/graph-projection.ts)

Three bugs that already exist on dev and that a general change hook makes easy to hit:

  • a callback that threw left the shared scheduler flushing forever, for every graph on the page - each callback is now isolated and the error is rethrown on a later task;
  • callbacks that keep scheduling each other were drained in one endless microtask - a flush now does at most 100 rounds, continues on a later task and logs one dev warning;
  • a graph change made by a subscriber during the notification was recorded and then wiped - the pending changes are now cleared before notifying.

Stories

flowchart, element-controls, collapsible-subtrees, dynamic-status-icons, introduction, automatic-layout and automatic-layout-storage use the new API.

Tests

  • src/store/__tests__/measurement.test.ts: the observer and graph-changes tests, ported one to one, plus the settled-state cases.
  • src/hooks/__tests__/measurement-events.test.tsx: the 16 spec cases from test(joint-react): specify how many events useOnElementsMeasured delivers #3520, driven through useOnCellsChange(selectMeasuredState, ...); measurement-settling.test.tsx; use-on-cells-change.test.tsx; use-on-elements-measured.test.tsx (deprecated hook, same assertions as before).
  • src/selectors/__tests__/source-selector.test.ts, src/utils/__tests__/scheduler.test.ts.
  • yarn test in packages/joint-react (typecheck + knip + duplicity-detect + jest): 1140 tests on React 19, 1136 on React 18, all passing. yarn lint: 0 errors.
  • The geometry of all 67 stories was compared in Chrome before and after: identical. yarn bench A/B is within noise.

Changesets:

  • @joint/react minor - useOnCellsChange - add the hook, with the selectMeasuredState, selectIsMeasured and selectElementsSizes selectors; useOnElementsMeasured is deprecated in favor of it.
  • @joint/react patch - <GraphProvider /> - fix a store callback that throws or changes the graph breaking later updates.

Motivation and Context

Follow-up of #3518 (spec in #3520). The measuring feature touched the core store files although most diagrams never use it, and useOnElementsMeasured was a one-purpose hook. With the measured state as a selector, the same need is covered by useCells and one general hook, and the store core no longer knows about measuring.

Migration (optional, the old hook still works):

// before
useOnElementsMeasured(({ isInitial, graph }) => { runLayout(graph); });

// after: every settled change
useOnCellsChange(selectMeasuredState, (measuredState) => {
  if (measuredState) runLayout(graph);
});

// after: once the sizes are known
const isMeasured = useCells(selectIsMeasured);

Notes

  • PRs to dev get no CI; the full suite was run locally.
  • Left for a follow-up: per-paper settling (elements seeded before any paper mounts, two papers where the non-measuring one mounts first) and the double mount call of a late-mounted useOnCellsChange under StrictMode (development only).
  • yarn test-coverage is below its floor on dev already; not changed here.

Screenshots (if appropriate):

- 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.
…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.
- Removed the export of create-elements-size-observer from index.ts.
- Introduced a new measurement.ts file to manage element size measurements.
- Updated paper-store.ts to integrate with the new measurement system.
- Modified state-container.ts to adjust import paths for selector utilities.
- Enhanced scheduler tests to handle callback errors and cascading callbacks.
- Replaced useOnElementsMeasured with useOnCellsChange in various examples and stories to reflect the new measurement approach.
- Added warnings for selector misuse and cascading callbacks in dev-warnings.ts.
- Updated examples to utilize the new selectMeasuredState selector for layout adjustments based on element sizes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant