Skip to content

Commit 07ba2ee

Browse files
authored
fix: fix for react strict mode (#107)
* fix: fix using graph with React StrictMode * feat: allow to set className for BlockList node. Allow to reach instance of react layer via react ref. * refactor: update Layer and LayersService for better performance and consistency
1 parent ee31950 commit 07ba2ee

File tree

26 files changed

+680
-237
lines changed

26 files changed

+680
-237
lines changed

.storybook/preview.ts

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

.storybook/preview.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Preview } from "@storybook/react";
2+
import React, { StrictMode } from "react";
3+
4+
import './styles/global.css';
5+
6+
const preview: Preview = {
7+
decorators: [
8+
(Story) => (
9+
<StrictMode>
10+
<Story />
11+
</StrictMode>
12+
),
13+
],
14+
};
15+
16+
export default preview;

docs/react/usage.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,20 @@ import { GraphCanvas } from "@gravity-ui/graph/react";
3434
graph={graph}
3535
renderBlock={renderBlock}
3636
className="my-graph"
37+
blockListClassName="custom-blocks-container"
38+
reactLayerRef={reactLayerRef}
3739
/>
3840
```
3941

42+
#### Props
43+
44+
- **`graph`** (required): The Graph instance to render
45+
- **`renderBlock`** (optional): Function to render custom block components
46+
- **`className`** (optional): CSS class for the main container
47+
- **`blockListClassName`** (optional): CSS class applied to the blocks container layer
48+
- **`reactLayerRef`** (optional): Ref to access the ReactLayer instance directly
49+
- **Event callbacks**: All graph event callbacks can be passed as props
50+
4051
### GraphBlock
4152

4253
The `GraphBlock` component is a crucial wrapper that handles the complex interaction between HTML elements and the canvas layer. It's responsible for:

src/components/canvas/blocks/Blocks.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ export class Blocks extends Component {
2727
}
2828

2929
protected rerender() {
30-
this.performRender();
30+
this.shouldRenderChildren = true;
3131
this.shouldUpdateChildren = true;
32+
this.performRender();
3233
}
3334

3435
protected subscribe() {

src/components/canvas/groups/BlockGroups.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -104,19 +104,6 @@ export class BlockGroups<P extends BlockGroupsProps = BlockGroupsProps> extends
104104
this.performRender = this.performRender.bind(this);
105105
}
106106

107-
/**
108-
* Called after initialization and when the layer is reattached.
109-
* This is where we set up event subscriptions to ensure they work properly
110-
* after the layer is unmounted and reattached.
111-
*/
112-
protected afterInit(): void {
113-
// Register event listener with the onGraphEvent method for automatic cleanup when unmounted
114-
this.onGraphEvent("camera-change", this.performRender);
115-
116-
// Call parent afterInit to ensure proper initialization
117-
super.afterInit();
118-
}
119-
120107
public getParent(): CoreComponent | undefined {
121108
/*
122109
* Override parent to delegate click events to camera.

src/components/canvas/layers/graphLayer/GraphLayer.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ export class GraphLayer extends Layer<TGraphLayerProps, TGraphLayerContext> {
104104

105105
// Subscribe to graph events here instead of in the constructor
106106
this.onGraphEvent("camera-change", this.performRender);
107+
this.context.graph.rootStore.blocksList.$blocks.subscribe(() => {
108+
this.performRender();
109+
});
110+
this.context.graph.rootStore.connectionsList.$connections.subscribe(() => {
111+
this.performRender();
112+
});
107113
super.afterInit();
108114
}
109115

src/graph.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,13 @@ export class Graph {
115115
this.setConstants(graphConstants);
116116
}
117117

118-
this.layers.on("update-size", (event: IRect) => {
119-
this.cameraService.set(event);
120-
});
121-
122118
this.setupGraph(config);
123119
}
124120

121+
protected onUpdateSize = (event: IRect) => {
122+
this.cameraService.set(event);
123+
};
124+
125125
public getGraphLayer() {
126126
return this.graphLayer;
127127
}
@@ -324,6 +324,7 @@ export class Graph {
324324
if (this.state === GraphState.READY) {
325325
return;
326326
}
327+
rootEl[Symbol.for("graph")] = this;
327328
this.layers.attach(rootEl);
328329

329330
const { width: rootWidth, height: rootHeight } = this.layers.getRootSize();
@@ -349,6 +350,7 @@ export class Graph {
349350
if (rootEl) {
350351
this.attach(rootEl);
351352
}
353+
this.layers.on("update-size", this.onUpdateSize);
352354
this.layers.start();
353355
this.scheduler.start();
354356
this.setGraphState(GraphState.READY);
@@ -395,9 +397,10 @@ export class Graph {
395397

396398
public unmount() {
397399
this.detach();
400+
this.layers.off("update-size", this.onUpdateSize);
398401
this.setGraphState(GraphState.INIT);
399402
this.hitTest.clear();
400-
this.layers.destroy();
403+
this.layers.unmount();
401404
clearTextCache();
402405
this.rootStore.reset();
403406
this.scheduler.stop();

src/graphConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export const initGraphConstants: TGraphConstants = {
133133
system: {
134134
GRID_SIZE: 16,
135135
/* @deprecated this config is not used anymore, Layers checks devicePixelRatio internally */
136-
PIXEL_RATIO: typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1,
136+
PIXEL_RATIO: typeof globalThis !== "undefined" ? globalThis.devicePixelRatio || 1 : 1,
137137
USABLE_RECT_GAP: 400,
138138
CAMERA_VIEWPORT_TRESHOLD: 0.5,
139139
},

src/plugins/devtools/DevToolsLayer.ts

Lines changed: 22 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ import "./devtools-layer.css"; // Import the CSS file after type imports
1919
*/
2020
export class DevToolsLayer extends Layer<TDevToolsLayerProps, LayerContext, TDevToolsLayerState> {
2121
public state = INITIAL_DEVTOOLS_LAYER_STATE;
22-
private mouseMoveListener = this.handleMouseMove.bind(this);
23-
private mouseEnterListener = this.handleMouseEnter.bind(this);
24-
private mouseLeaveListener = this.handleMouseLeave.bind(this);
25-
protected cameraSubscription: (() => void) | null = null;
2622

2723
// HTML elements for ruler backgrounds
2824
private horizontalRulerBgEl: HTMLDivElement | null = null;
@@ -77,22 +73,27 @@ export class DevToolsLayer extends Layer<TDevToolsLayerProps, LayerContext, TDev
7773
}
7874

7975
protected afterInit(): void {
80-
super.afterInit();
81-
82-
if (this.context.graph) {
83-
this.cameraSubscription = this.context.graph.on("camera-change", () => this.performRender());
84-
} else {
85-
console.error("DevToolsLayer: Graph instance not found in context during afterInit.");
86-
}
87-
88-
const graphRoot = this.props.graph?.layers?.$root;
89-
if (graphRoot) {
90-
graphRoot.addEventListener("mousemove", this.mouseMoveListener);
91-
graphRoot.addEventListener("mouseenter", this.mouseEnterListener);
92-
graphRoot.addEventListener("mouseleave", this.mouseLeaveListener);
93-
} else {
94-
console.error("DevToolsLayer: Graph root layer element ($root) not found.");
95-
}
76+
this.onGraphEvent("camera-change", () => this.performRender());
77+
this.onRootEvent(
78+
"mousemove",
79+
(event: MouseEvent): void => {
80+
const canvas = this.context.graphCanvas;
81+
if (!canvas) return;
82+
const rect = canvas.getBoundingClientRect();
83+
this.setState({
84+
mouseX: event.clientX - rect.left,
85+
mouseY: event.clientY - rect.top,
86+
isMouseInside: true,
87+
});
88+
},
89+
{ capture: true }
90+
);
91+
this.onRootEvent("mouseenter", (): void => {
92+
this.setState({ isMouseInside: true });
93+
});
94+
this.onRootEvent("mouseleave", (): void => {
95+
this.setState({ isMouseInside: false, mouseX: null, mouseY: null });
96+
});
9697

9798
// Create HTML elements for ruler backgrounds if showRuler is initially true
9899
const htmlContainer = this.getHTML();
@@ -123,61 +124,9 @@ export class DevToolsLayer extends Layer<TDevToolsLayerProps, LayerContext, TDev
123124
htmlContainer.appendChild(this.verticalRulerBgEl);
124125
}
125126

126-
this.performRender(); // Initial render for positioning divs and canvas ticks/text
127-
}
128-
129-
protected unmountLayer(): void {
130-
this.cameraSubscription?.();
131-
this.cameraSubscription = null;
132-
133-
const graphRoot = this.props.graph?.layers?.$root;
134-
if (graphRoot) {
135-
graphRoot.removeEventListener("mousemove", this.mouseMoveListener);
136-
graphRoot.removeEventListener("mouseenter", this.mouseEnterListener);
137-
graphRoot.removeEventListener("mouseleave", this.mouseLeaveListener);
138-
}
139-
140-
// Remove ruler background elements
141-
this.horizontalRulerBgEl?.remove();
142-
this.verticalRulerBgEl?.remove();
143-
this.horizontalRulerBgEl = null;
144-
this.verticalRulerBgEl = null;
145-
146-
super.unmountLayer();
147-
}
148-
149-
// --- Event Handlers ---
150-
151-
private handleMouseMove(event: MouseEvent): void {
152-
const canvas = this.context.graphCanvas;
153-
if (!canvas) return;
154-
const rect = canvas.getBoundingClientRect();
155-
this.setState({
156-
mouseX: event.clientX - rect.left,
157-
mouseY: event.clientY - rect.top,
158-
isMouseInside: true,
159-
});
160-
}
161-
162-
private handleMouseEnter(): void {
163-
this.setState({ isMouseInside: true });
164-
}
165-
166-
private handleMouseLeave(): void {
167-
this.setState({ isMouseInside: false, mouseX: null, mouseY: null });
168-
}
169-
170-
// --- State Update ---
171-
172-
protected stateChanged(): void {
173-
// Only re-render canvas if crosshair is visible and mouse position changed
174-
if (this.props.showCrosshair) {
175-
this.performRender();
176-
}
127+
super.afterInit();
177128
}
178129

179-
// --- Rendering Logic ---
180-
181130
protected render(): void {
182131
if (!this.context.ctx || !this.context.graphCanvas) {
183132
return;

src/plugins/minimap/layer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,16 @@ export class MiniMapLayer extends Layer<MiniMapLayerProps, MiniMapLayerContext>
9292
this.onCanvasEvent("mousedown", this.handleMouseDownEvent);
9393
}
9494
}
95+
this.onSignal(this.props.graph.hitTest.$usableRect, () => {
96+
this.onBlockUpdated();
97+
this.calculateViewPortCoords();
98+
this.rerenderMapContent();
99+
});
95100

96101
super.afterInit();
97102
}
98103

99-
public updateSize(): void {
104+
protected updateCanvasSize(): void {
100105
this.rerenderMapContent();
101106
}
102107

0 commit comments

Comments
 (0)