Skip to content

Commit ca0e968

Browse files
committed
fix ref attr in konva node, fix issues in strict mode. close #761, #834
1 parent aa7c337 commit ca0e968

File tree

5 files changed

+118
-88
lines changed

5 files changed

+118
-88
lines changed

ReactKonvaCore.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// special file for minimal import
22
import * as React from 'react';
3-
import * as ReactReconciler from 'react-reconciler'
3+
import * as ReactReconciler from 'react-reconciler';
44
import Konva from 'konva';
55

66
export interface KonvaNodeEvents {
@@ -102,4 +102,4 @@ export var Arrow: KonvaNodeComponent<Konva.Arrow, Konva.ArrowConfig>;
102102
export var Shape: KonvaNodeComponent<Konva.Shape, Konva.ShapeConfig>;
103103

104104
export var useStrictMode: (useStrictMode: boolean) => void;
105-
export var KonvaRenderer: ReactReconciler.Reconciler<any, any, any, any, any>
105+
export var KonvaRenderer: ReactReconciler.Reconciler<any, any, any, any, any>;

package.json

Lines changed: 1 addition & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1 @@
1-
{
2-
"license": "MIT",
3-
"name": "react-konva",
4-
"description": "React binding to canvas element via Konva framework",
5-
"version": "19.0.3",
6-
"keywords": [
7-
"react",
8-
"canvas",
9-
"jsx",
10-
"konva"
11-
],
12-
"bugs": "https://github.com/konvajs/react-konva/issues",
13-
"main": "lib/ReactKonva.js",
14-
"module": "es/ReactKonva.js",
15-
"repository": {
16-
"type": "git",
17-
"url": "git@github.com:konvajs/react-konva.git"
18-
},
19-
"dependencies": {
20-
"@types/react-reconciler": "^0.28.9",
21-
"its-fine": "^2.0.0",
22-
"scheduler": "0.25.0",
23-
"react-reconciler": "0.31.0"
24-
},
25-
"targets": {
26-
"none": {}
27-
},
28-
"funding": [
29-
{
30-
"type": "patreon",
31-
"url": "https://www.patreon.com/lavrton"
32-
},
33-
{
34-
"type": "opencollective",
35-
"url": "https://opencollective.com/konva"
36-
},
37-
{
38-
"type": "github",
39-
"url": "https://github.com/sponsors/lavrton"
40-
}
41-
],
42-
"peerDependencies": {
43-
"konva": "^8.0.1 || ^7.2.5 || ^9.0.0",
44-
"react": "^18.3.1 || ^19.0.0",
45-
"react-dom": "^18.3.1 || ^19.0.0"
46-
},
47-
"devDependencies": {
48-
"@types/chai": "^5.0.1",
49-
"@types/mocha": "^10.0.10",
50-
"@types/react": "19.0.10",
51-
"assert": "^2.1.0",
52-
"chai": "5.2.0",
53-
"konva": "^9.3.18",
54-
"mocha-headless-chrome": "^4.0.0",
55-
"parcel": "^2.13.3",
56-
"process": "^0.11.10",
57-
"react": "^19.0.0",
58-
"react-dom": "^19.0.0",
59-
"sinon": "^19.0.2",
60-
"timers-browserify": "^2.0.12",
61-
"typescript": "^5.7.3",
62-
"use-image": "^1.1.1",
63-
"util": "^0.12.5"
64-
},
65-
"scripts": {
66-
"build": "tsc -outDir ./es && tsc -module commonjs -outDir ./lib && cp ./ReactKonvaCore.d.ts ./lib && cp ./ReactKonvaCore.d.ts ./es",
67-
"test:typings": "tsc --noEmit",
68-
"preversion": "npm test",
69-
"version": "npm run build",
70-
"postversion": "",
71-
"test": "NODE_ENV=test npm run test:build && mocha-headless-chrome -f ./test-build/unit-tests.html -a disable-web-security && npm run test:typings",
72-
"test:build": "rm -rf ./.parcel-cache && NODE_ENV=test parcel build ./test/unit-tests.html --dist-dir test-build --target none --public-url ./ --no-source-maps",
73-
"test:watch": "NODE_ENV=test rm -rf ./parcel-cache && parcel serve ./test/unit-tests.html"
74-
},
75-
"typings": "react-konva.d.ts",
76-
"files": [
77-
"README.md",
78-
"lib",
79-
"es",
80-
"react-konva.d.ts",
81-
"ReactKonvaCore.d.ts"
82-
]
83-
}
1+
{}

src/ReactKonvaCore.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import ReactFiberReconciler, {
2525
import { ConcurrentRoot } from 'react-reconciler/constants.js';
2626
import * as HostConfig from './ReactKonvaHostConfig.js';
2727
import { applyNodeProps, toggleStrictMode } from './makeUpdates.js';
28-
import { useContextBridge, FiberProvider } from 'its-fine';
28+
import { useContextBridge, FiberProvider, useFiber } from 'its-fine';
2929
import { Container } from 'konva/lib/Container.js';
3030

3131
/**
@@ -66,13 +66,23 @@ function usePrevious(value) {
6666
return ref.current;
6767
}
6868

69+
const useIsReactStrictMode = () => {
70+
const memoCount = React.useRef(0);
71+
// in strict mode, memo will be called twice
72+
React.useMemo(() => {
73+
memoCount.current++;
74+
}, []);
75+
return memoCount.current > 1;
76+
};
77+
6978
const StageWrap = (props) => {
7079
const container = React.useRef(null);
7180
const stage = React.useRef<any>(null);
7281
const fiberRef = React.useRef(null);
7382

7483
const oldProps = usePrevious(props);
7584
const Bridge = useContextBridge();
85+
const isMounted = React.useRef(false);
7686

7787
const _setRef = (stage) => {
7888
const { forwardedRef } = props;
@@ -86,7 +96,20 @@ const StageWrap = (props) => {
8696
}
8797
};
8898

99+
const isStrictMode = useIsReactStrictMode();
100+
89101
React.useLayoutEffect(() => {
102+
// is we are in strict mode, we need to ignore the second full render
103+
// instead do nothing and just return clean function
104+
if (isMounted.current && isStrictMode) {
105+
return () => {
106+
isMounted.current = false;
107+
_setRef(null);
108+
KonvaRenderer.updateContainer(null, fiberRef.current, null);
109+
stage.current.destroy();
110+
};
111+
}
112+
isMounted.current = true;
90113
stage.current = new Konva.Stage({
91114
width: props.width,
92115
height: props.height,
@@ -117,6 +140,10 @@ const StageWrap = (props) => {
117140
);
118141

119142
return () => {
143+
// inside React strict mode, we need to ignore cleanup, because it will mess with refs
144+
if (isStrictMode) {
145+
return;
146+
}
120147
_setRef(null);
121148
KonvaRenderer.updateContainer(null, fiberRef.current, null);
122149
stage.current.destroy();

src/ReactKonvaHostConfig.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ export function createInstance(type, props, internalInstanceHandle) {
5858
const propsWithOnlyEvents = {};
5959

6060
for (var key in props) {
61+
// ignore ref
62+
if (key === 'ref') {
63+
continue;
64+
}
6165
var isEvent = key.slice(0, 2) === 'on';
6266
if (isEvent) {
6367
propsWithOnlyEvents[key] = props[key];

test/react-konva-test.tsx

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// @ts-nocheck
12
import React from 'react';
23
import { createRoot } from 'react-dom/client';
34
import sinon from 'sinon';
@@ -15,10 +16,28 @@ import {
1516
Text,
1617
Image,
1718
Circle,
19+
Transformer,
1820
} from '../src/ReactKonva';
1921

2022
global.IS_REACT_ACT_ENVIRONMENT = true;
2123

24+
// Suppress console warnings about act() for custom reconciler updates
25+
const originalConsoleError = console.error;
26+
const originalConsoleWarn = console.warn;
27+
28+
console.error = (...args) => {
29+
const message = args[0];
30+
if (
31+
typeof message === 'string' &&
32+
(message.includes('was not wrapped in act') ||
33+
message.includes('suspended resource finished loading'))
34+
) {
35+
// Suppress act warnings for custom reconciler
36+
return;
37+
}
38+
originalConsoleError.apply(console, args);
39+
};
40+
2241
const render = async (component) => {
2342
const node = document.createElement('div');
2443
document.body.appendChild(node);
@@ -89,6 +108,7 @@ describe('initial mounting and refs', () => {
89108
expect(stageRef.current instanceof Konva.Stage).to.be.true;
90109
expect(layerRef.current instanceof Konva.Layer).to.be.true;
91110
expect(rectRef.current instanceof Konva.Rect).to.be.true;
111+
expect(rectRef.current.getAttr('ref')).to.be.undefined;
92112
});
93113

94114
it('no fail on no ref', async () => {
@@ -619,6 +639,9 @@ describe('Test Events', async function () {
619639

620640
describe('Bad structure', () => {
621641
it('No dom inside Konva', async function () {
642+
const originalError = console.error;
643+
const error = sinon.spy();
644+
console.error = error;
622645
class App extends React.Component {
623646
render() {
624647
return (
@@ -632,6 +655,11 @@ describe('Bad structure', () => {
632655
}
633656

634657
const { stage } = await render(<App />);
658+
expect(error.callCount).to.equal(1);
659+
expect(error.firstCall.args[0]).to.include(
660+
'Konva has no node with the type div'
661+
);
662+
console.error = originalError;
635663
// check check that this test is not failed
636664
});
637665
});
@@ -1253,7 +1281,7 @@ describe('Hooks', async function () {
12531281
});
12541282

12551283
it('check useImage hook', async function () {
1256-
const url = 'https://konvajs.org/favicon-32x32.png?token' + Math.random();
1284+
const url = 'https://konvajs.org//img/icon.png?token' + Math.random();
12571285

12581286
const App = () => {
12591287
const [image, status] = useImage(url);
@@ -1287,7 +1315,7 @@ describe('Hooks', async function () {
12871315
});
12881316

12891317
it('unsubscribe on unmount', async function () {
1290-
const url = 'https://konvajs.org/favicon-32x32.png';
1318+
const url = 'https://konvajs.org//img/icon.png';
12911319

12921320
const App = () => {
12931321
const [image, status] = useImage(url);
@@ -1336,6 +1364,59 @@ describe('external', () => {
13361364
});
13371365
});
13381366

1367+
describe('React StrictMode', () => {
1368+
it('make sure effect is called AFTER we set refs of konva nodes', async function () {
1369+
const App = () => {
1370+
const stageRef = React.useRef<Konva.Stage>(null);
1371+
const [count, setCount] = React.useState(0);
1372+
const shapeRef = React.useRef<Konva.Rect>(null);
1373+
const trRef = React.useRef<Konva.Transformer>(null);
1374+
1375+
const isMounted = React.useRef(false);
1376+
1377+
React.useEffect(() => {
1378+
setCount(1);
1379+
setTimeout(() => {
1380+
setCount(2);
1381+
setTimeout(() => {
1382+
setCount(3);
1383+
}, 10);
1384+
}, 10);
1385+
}, []);
1386+
1387+
React.useEffect(() => {
1388+
// we need to attach transformer manually
1389+
trRef.current?.nodes([shapeRef.current]);
1390+
trRef.current?.getLayer().batchDraw();
1391+
}, [count]);
1392+
1393+
return (
1394+
<>
1395+
{count !== 2 && (
1396+
<Stage width={300} height={300} ref={stageRef}>
1397+
<Layer>
1398+
<Rect fill="red" ref={shapeRef} />
1399+
<Transformer ref={trRef} />
1400+
</Layer>
1401+
</Stage>
1402+
)}
1403+
</>
1404+
);
1405+
};
1406+
1407+
const { stage } = await render(
1408+
<React.StrictMode>
1409+
<App />
1410+
</React.StrictMode>
1411+
);
1412+
await new Promise((resolve) => setTimeout(resolve, 100));
1413+
const lastStage = Konva.stages[Konva.stages.length - 1];
1414+
const lastTransformer = lastStage.findOne('Transformer');
1415+
expect(lastTransformer.nodes().length).to.equal(1);
1416+
expect(lastTransformer.nodes()[0].fill()).to.equal('red');
1417+
});
1418+
});
1419+
13391420
// reference for the test: https://github.com/konvajs/react-konva/issues/748
13401421
// TODO: can we fix that?
13411422
describe.skip('update order', () => {

0 commit comments

Comments
 (0)