Skip to content

Commit 8d9dbc0

Browse files
committed
feat(joint-react): update dependencies, refactor components, and enhance performance
- Updated dependencies in yarn.lock, including Babel and added unplugin. - Refactored GraphProvider and Paper components for improved performance and clarity. - Removed unused files and hooks to streamline the codebase. - Enhanced the Storybook setup with a new script for loading react-scan. - Improved element rendering logic and optimized hooks for better state management. - Added tests for new functionalities and ensured existing tests are up to date.
1 parent 0fe069f commit 8d9dbc0

File tree

115 files changed

+7365
-3862
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

115 files changed

+7365
-3862
lines changed

packages/joint-react-eslint/eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ const config = [
170170
"error",
171171
{
172172
replacements: {
173+
dev: false,
173174
doc: false,
174175
Props: false,
175176
props: false,
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,58 @@
11
<!-- Loads a font from a CDN -->
2+
<!-- react-scan: Load with delay to avoid freezing Storybook -->
3+
<script>
4+
// Load react-scan UI overlay after Storybook initializes
5+
(function () {
6+
if (typeof window === 'undefined') return;
7+
8+
// Wait for DOM and React to be ready
9+
function loadReactScan() {
10+
if (window.__REACT_SCAN_LOADED) return;
11+
12+
const script = document.createElement('script');
13+
script.src = 'https://unpkg.com/react-scan@latest/dist/auto.global.js';
14+
script.async = true;
15+
script.onload = function () {
16+
window.__REACT_SCAN_LOADED = true;
17+
18+
19+
// Wait a bit for react-scan to initialize, then configure it for iframe
20+
setTimeout(function () {
21+
// Access react-scan global API
22+
const reactScan = window.reactScan;
23+
24+
if (reactScan && typeof reactScan === 'function') {
25+
// Configure react-scan to work in iframe (Storybook uses iframes)
26+
try {
27+
reactScan({
28+
enabled: false, // Disabled by default - user can enable via toolbar
29+
allowInIframe: true,
30+
showToolbar: true, // Show toolbar so user can toggle it on/off
31+
});
32+
33+
} catch (error) {
34+
console.error('❌ Error configuring react-scan:', error);
35+
}
36+
} else {
37+
console.warn('⚠️ react-scan function not found');
38+
}
39+
}, 500);
40+
};
41+
script.onerror = function () {
42+
console.error('❌ Failed to load react-scan');
43+
};
44+
document.head.appendChild(script);
45+
}
46+
47+
// Try loading after a delay
48+
if (document.readyState === 'loading') {
49+
document.addEventListener('DOMContentLoaded', function () {
50+
setTimeout(loadReactScan, 3000);
51+
});
52+
} else {
53+
setTimeout(loadReactScan, 3000);
54+
}
55+
})();
56+
</script>
257
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
358
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">

packages/joint-react/.storybook/preview.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import './wdyr';
21
import type { Preview } from '@storybook/react';
32
import { withPerformance } from 'storybook-addon-performance';
43
import { theme } from './theme';

packages/joint-react/.storybook/wdyr.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

packages/joint-react/README.md

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ Subscribe to pointer events on elements/links.
160160
### 4) Controlled updates (React state drives the graph)
161161
Pass `elements/links` + `onElementsChange/onLinksChange` to keep React in charge.
162162

163+
**React-controlled mode** gives you full control over graph state, enabling features like undo/redo, persistence, and integration with other React state management.
164+
163165
```tsx
164166
import React, { useState } from 'react'
165167
import { GraphProvider } from '@joint/react'
@@ -187,7 +189,78 @@ export function Controlled() {
187189
}
188190
```
189191

190-
### 5) Imperative access (ref) for one‑off actions
192+
### 5) External store integration (Redux, Zustand, etc.)
193+
Use `externalStore` prop to integrate with external state management libraries.
194+
195+
```tsx
196+
import { GraphProvider } from '@joint/react'
197+
import { createStore } from 'zustand'
198+
199+
// Create a store compatible with ExternalStoreLike interface
200+
const useGraphStore = createStore((set) => ({
201+
elements: [],
202+
links: [],
203+
setState: (updater) => set(updater),
204+
getSnapshot: () => useGraphStore.getState(),
205+
subscribe: (listener) => {
206+
const unsubscribe = useGraphStore.subscribe(listener)
207+
return unsubscribe
208+
}
209+
}))
210+
211+
export function ExternalStoreExample() {
212+
const store = useGraphStore()
213+
214+
return (
215+
<GraphProvider externalStore={store}>
216+
<Paper height={320} />
217+
</GraphProvider>
218+
)
219+
}
220+
```
221+
222+
### 6) Programmatic cell manipulation
223+
Use the `useCellActions` hook to programmatically add, update, and remove cells.
224+
225+
```tsx
226+
import { useCellActions } from '@joint/react'
227+
228+
function MyComponent() {
229+
const { set, remove } = useCellActions()
230+
231+
const addNode = () => {
232+
set({
233+
id: 'new-node',
234+
x: 100,
235+
y: 100,
236+
width: 120,
237+
height: 60,
238+
label: 'New Node'
239+
})
240+
}
241+
242+
const updateNode = () => {
243+
set('new-node', (prev) => ({
244+
...prev,
245+
label: 'Updated'
246+
}))
247+
}
248+
249+
const deleteNode = () => {
250+
remove('new-node')
251+
}
252+
253+
return (
254+
<div>
255+
<button onClick={addNode}>Add</button>
256+
<button onClick={updateNode}>Update</button>
257+
<button onClick={deleteNode}>Delete</button>
258+
</div>
259+
)
260+
}
261+
```
262+
263+
### 7) Imperative access (ref) for one‑off actions
191264
Useful for `fitToContent`, scaling, exporting.
192265

193266
```tsx
@@ -221,24 +294,36 @@ export function FitOnMount() {
221294
- **Prefer declarative first**: Reach for hooks/props; use imperative APIs (refs/graph methods) for targeted operations only.
222295
- **Test in Safari early** when using `<foreignObject>`; fall back to `useHTMLOverlay` if needed.
223296
- **Accessing component instances via refs**: Any component that accepts a `ref` (such as `Paper` or `GraphProvider`) exposes its instance/context via the ref. For `Paper`, the instance (including the underlying JointJS Paper) can be accessed via the `paperCtx` property on the ref object.
297+
- **Choose the right mode**: Use uncontrolled mode for simple cases, React-controlled for full state control, and external-store for integration with Redux/Zustand.
298+
- **Use selectors efficiently**: When using `useElements` or `useLinks`, provide custom selectors and equality functions to minimize re-renders.
299+
- **Batch updates**: The library automatically batches updates, but be mindful of rapid state changes in controlled mode.
224300

225301
---
226302

227303
## ⚙️ API Surface (at a glance)
228304

229305
- **Components**
230-
- `GraphProvider` — provides the shared graph
231-
- `Paper` — renders the graph (Paper)
306+
- `GraphProvider` — provides the shared graph context
307+
- `Paper` — renders the graph (Paper view)
232308

233309
- **Hooks**
234-
- `useElements()` / `useLinks()` — subscribe to data
235-
- `useGraph()` — low-level graph access
236-
- `usePaper()` — access the underlying Paper (from within a view)
310+
- `useElements()` / `useLinks()` — subscribe to elements/links with optional selectors
311+
- `useGraph()` — access the underlying JointJS graph instance
312+
- `usePaper()` — access the underlying Paper instance (from within a Paper view)
313+
- `useCellActions()` — programmatically add, update, and remove cells
314+
315+
- **Controlled mode props** (React-controlled)
316+
- `elements`, `links` — current state
317+
- `onElementsChange`, `onLinksChange` — state update callbacks
318+
319+
- **External store mode**
320+
- `externalStore` — external state management store (Redux, Zustand, etc.)
237321

238-
- **Controlled mode props**
239-
- `elements`, `links`, `onElementsChange`, `onLinksChange`
322+
- **Uncontrolled mode** (default)
323+
- `initialElements`, `initialLinks` — initial values only
324+
- Graph manages its own state internally
240325

241-
> Tip: You can pass an existing JointJS `dia.Graph` into `GraphProvider` if you need to integrate with external data lifecycles.
326+
> **Tip:** You can pass an existing JointJS `dia.Graph` into `GraphProvider` if you need to integrate with external data lifecycles or share a graph across multiple providers.
242327
243328
---
244329

packages/joint-react/jest.config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/** @type {import('ts-jest').JestConfigWithTsJest} **/
21
export default {
32
testEnvironment: 'jsdom',
43
testPathIgnorePatterns: ['/node_modules/', '/dist/'],

packages/joint-react/package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,23 +73,27 @@
7373
"@types/react": "19.1.12",
7474
"@types/react-dom": "19.1.9",
7575
"@types/react-test-renderer": "19.1.0",
76+
"@types/scheduler": "^0",
7677
"@types/use-sync-external-store": "1.5.0",
7778
"@vitejs/plugin-react": "^5.0.2",
78-
"@welldone-software/why-did-you-render": "10.0.1",
7979
"canvas": "^3.1.0",
8080
"eslint": "9.33.0",
8181
"glob": "^11.0.1",
8282
"jest": "30.1.2",
8383
"jest-environment-jsdom": "30.1.2",
84+
"jotai": "^2.15.2",
8485
"knip": "5.63.0",
86+
"peerjs": "^1.5.5",
8587
"prettier": "3.3.3",
8688
"prettier-plugin-tailwindcss": "^0.6.5",
8789
"react": "^19.1.1",
8890
"react-docgen-typescript-plugin": "^1.0.8",
8991
"react-dom": "^19.1.1",
9092
"react-redux": "^9.2.0",
93+
"react-scan": "^0.4.3",
9194
"react-test-renderer": "^19.1.1",
9295
"redux": "^5.0.1",
96+
"redux-undo": "1.1.0",
9397
"storybook": "^8.6.14",
9498
"storybook-addon-performance": "0.17.3",
9599
"storybook-multilevel-sort": "2.1.0",
@@ -104,10 +108,12 @@
104108
"vite-plugin-md": "0.22.5",
105109
"vite-plugin-node-polyfills": "^0.24.0",
106110
"vite-tsconfig-paths": "^5.1.4",
107-
"vitest": "^3.0.4"
111+
"vitest": "^3.0.4",
112+
"zustand": "^5.0.9"
108113
},
109114
"dependencies": {
110115
"@joint/core": "workspace:*",
116+
"scheduler": "^0.27.0",
111117
"use-sync-external-store": "^1.4.0"
112118
},
113119
"peerDependencies": {

packages/joint-react/src/components/graph/graph-provider.stories.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,7 @@ function Component() {
121121
setIsReady(true);
122122
}, 1000);
123123
}, []);
124-
return (
125-
isReady && (
126-
<Paper interactive={false} className={PAPER_CLASSNAME} renderElement={RenderHTMLElement} />
127-
)
128-
);
124+
return isReady && <Paper className={PAPER_CLASSNAME} renderElement={RenderHTMLElement} />;
129125
}
130126

131127
export const ConditionalRender: Story = {

0 commit comments

Comments
 (0)