Skip to content

feat(joint-react): add useCollection / useSetCollection hooks#3309

Open
samuelgja wants to merge 7 commits into
clientIO:devfrom
samuelgja:feat/joint-react-collections
Open

feat(joint-react): add useCollection / useSetCollection hooks#3309
samuelgja wants to merge 7 commits into
clientIO:devfrom
samuelgja:feat/joint-react-collections

Conversation

@samuelgja
Copy link
Copy Markdown
Contributor

Description

Adds two new React hooks to @joint/react:

  • useCollection(collection?, options?) / useCollection(collection?, selector, options?) — subscribe to any mvc.Collection<dia.Cell> (selection, clipboard, custom watchlists). Returns [cells, setCells] (or [selected, setCells]). Options: onChange, isEqual. Built on useSyncExternalStoreWithSelector.
  • useSetCollection(collection?) — write-only sibling that returns the same setter without a subscription.

The setter accepts full CellRecord objects (direct array or updater form). When a record's id matches an existing graph cell, attributes are merged into that cell inside a graph.startBatch/stopBatch window so changes flow through JointJS' normal change pipeline. Records for missing ids construct new cell instances via the graph's cellNamespace.

Internals:

  • New state/data-mapping/cell-record-merge.ts — extracted mergeCellRecord / toCellRecord / writeCellToContainer (shared by graph-view and the new collection-view).
  • New store/collection-view.ts — mirrors an mvc.Collection<dia.Cell> into a records container using the same createContainer + mvc.Listener pattern as the main graph view.
  • GraphStore gains acquireCollectionView / releaseCollectionView (refcount-cached, evicted on zero subscribers, torn down in destroy()).
  • ArrayUpdate<T> type alias added next to Update<T> / AtomUpdate<T> in state-container.ts; useResetCells refactored to use it.
  • Array-equality helpers (areArraysShallowEqual, arrayAwareEqual) hoisted from use-cells.ts to utils/selector-utils.ts.

Tests: 22 new tests covering reactivity on data / attrs / size, selector form (records → ids), full-record setter under selector form, swap from undefined → defined collection, ref-counted view registry, batched cell-attribute setter.

Storybook smoke: an interactive "Watchlist" demo (click to toggle membership, drag for live position updates, Shuffle / Resize / Clear via the setter, derived Σ area via selector).

Motivation and Context

@joint/react-plus already shipped an id-based useCollection. Bringing a records-based version into @joint/react removes the duplicated subscription plumbing across consumers (selection, clipboard, custom collections) and aligns the API with useCells so collection contents are first-class records rather than just ids — including reactive inner properties.

samuelgja and others added 2 commits May 12, 2026 14:49
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tions

- Implemented `useCollection` hook to subscribe to JointJS collection cells as `CellRecord` instances, supporting both direct and selector forms.
- Created `useSetCollection` hook to replace contents of a JointJS collection with supplied records, supporting both direct and updater forms.
- Introduced `createCollectionView` function to mirror a JointJS collection into a read-only records container, handling events like add, remove, change, and reset.
- Added tests for collection view functionality, ensuring correct behavior on collection mutations and attribute changes.
- Refactored data mapping utilities to support merging cell records and writing cells to containers, preserving reference identity where possible.
- Updated GraphStore to manage collection views, ensuring proper cleanup and reference counting.
@samuelgja samuelgja requested a review from kumilingus May 12, 2026 10:22
samuelgja added 5 commits May 13, 2026 15:44
…ng JointJS collections

- Implemented `useCellCollection` to subscribe to JointJS collections and return cell records or selected values based on a selector.
- Introduced `useSetCellCollection` to replace contents of a JointJS collection with records or dia.Cell instances.
- Enhanced cell input normalization with `normalizeCellInput` utility to handle both plain records and dia.Cell instances.
- Updated related hooks and types to support new functionality, ensuring compatibility with existing code.
- Added tests for `normalizeCellInput` to verify behavior with various input types.
* @param b - next result (expected to be a readonly array)
* @returns true when both arrays match by length and element identity
*/
function arrayResultEqual(a: unknown, b: unknown): boolean {
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.

is this wrapper necessary?

* @param graph - graph used for cell lookup and type-constructor resolution
* @returns the resolved `dia.Cell` (existing or freshly constructed)
*/
function resolveRecordToCell(record: AnyCellRecord, graph: dia.Graph): dia.Cell {
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.

I don't like this method.

  • if it exists - it modifies it
  • if it does not exist - it does not add it to graph

try {
expect(paper).toBeInstanceOf(dia.Paper);
expect(paper.el.classList.contains('jj-paper')).toBe(true);
expect(paper.el.classList.contains('joint-jj-paper')).toBe(true);
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.

this is wrong. revert it please. You need the latest joint-react

* @param minPathMargin - minimum path margin used by the underlying router.
*/
export function rightAngleRouter(margin: number, minPathMargin: number): routers.Router {
export function rightAngleRouter(margin: number = 0, minPathMargin: number = 0): routers.Router {
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.

here we go again :)

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