From c7ad83e6405d2d1151803f68e7ea08ce44af3dce Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Fri, 3 May 2024 15:22:12 -0400 Subject: [PATCH] Make host config compatible with React 19 Beta --- .gitignore | 3 +- package.json | 22 +++---- src/ReactKonvaCore.tsx | 2 +- src/ReactKonvaHostConfig.ts | 62 ++++++++++++++++-- test/react-konva-test.tsx | 122 +++++++++++++++++++----------------- 5 files changed, 138 insertions(+), 73 deletions(-) diff --git a/.gitignore b/.gitignore index db745b7..f7c4f38 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ es test-build .parcel-cache dist -package-lock.json \ No newline at end of file +package-lock.json +.vscode diff --git a/package.json b/package.json index 65b175a..1ccefad 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "license": "MIT", "name": "react-konva", "description": "React binding to canvas element via Konva framework", - "version": "18.2.10", + "version": "19.0.0", "keywords": [ "react", "canvas", @@ -18,9 +18,9 @@ }, "dependencies": { "@types/react-reconciler": "^0.28.2", - "its-fine": "^1.1.1", - "react-reconciler": "~0.29.0", - "scheduler": "^0.23.0" + "its-fine": "^1.2.5", + "react-reconciler": "0.31.0-beta-73bcdfbae5-20240502", + "scheduler": "0.25.0-beta-73bcdfbae5-20240502" }, "targets": { "none": {} @@ -41,8 +41,8 @@ ], "peerDependencies": { "konva": "^8.0.1 || ^7.2.5 || ^9.0.0", - "react": ">=18.0.0", - "react-dom": ">=18.0.0" + "react": ">=19.0.0-beta", + "react-dom": ">=19.0.0-beta" }, "devDependencies": { "@types/chai": "^4.3.5", @@ -53,8 +53,8 @@ "mocha-headless-chrome": "^4.0.0", "parcel": "^2.9.1", "process": "^0.11.10", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "19.0.0-beta-73bcdfbae5-20240502", + "react-dom": "19.0.0-beta-73bcdfbae5-20240502", "sinon": "^15.1.0", "timers-browserify": "^2.0.12", "typescript": "^5.1.3", @@ -67,9 +67,9 @@ "preversion": "npm test", "version": "npm run build", "postversion": "", - "test": "npm run test:build && mocha-headless-chrome -f ./test-build/unit-tests.html -a disable-web-security && npm run test:typings", - "test:build": "parcel build ./test/unit-tests.html --dist-dir test-build --target none --public-url ./ --no-source-maps", - "test:watch": "rm -rf ./parcel-cache && parcel serve ./test/unit-tests.html" + "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", + "test:build": "NODE_ENV=test parcel build ./test/unit-tests.html --dist-dir test-build --target none --public-url ./ --no-source-maps", + "test:watch": "NODE_ENV=test rm -rf ./parcel-cache && parcel serve ./test/unit-tests.html" }, "typings": "react-konva.d.ts", "files": [ diff --git a/src/ReactKonvaCore.tsx b/src/ReactKonvaCore.tsx index b06b5e7..b6467b5 100644 --- a/src/ReactKonvaCore.tsx +++ b/src/ReactKonvaCore.tsx @@ -10,7 +10,7 @@ import React from 'react'; import Konva from 'konva/lib/Core.js'; import ReactFiberReconciler from 'react-reconciler'; -import { LegacyRoot, ConcurrentRoot } from 'react-reconciler/constants.js'; +import { LegacyRoot } from 'react-reconciler/constants.js'; import * as HostConfig from './ReactKonvaHostConfig.js'; import { applyNodeProps, toggleStrictMode } from './makeUpdates.js'; import { useContextBridge, FiberProvider } from 'its-fine'; diff --git a/src/ReactKonvaHostConfig.ts b/src/ReactKonvaHostConfig.ts index a478346..b825825 100644 --- a/src/ReactKonvaHostConfig.ts +++ b/src/ReactKonvaHostConfig.ts @@ -1,12 +1,19 @@ import Konva from 'konva/lib/Core.js'; -import { applyNodeProps, updatePicture, EVENTS_NAMESPACE } from './makeUpdates.js'; +import { + applyNodeProps, + updatePicture, + EVENTS_NAMESPACE, +} from './makeUpdates.js'; export { unstable_now as now, unstable_IdlePriority as idlePriority, unstable_runWithPriority as run, } from 'scheduler'; -import { DefaultEventPriority } from 'react-reconciler/constants.js'; +import { + NoEventPriority, + DefaultEventPriority, +} from 'react-reconciler/constants.js'; const NO_CONTEXT = {}; const UPDATE_SIGNAL = {}; @@ -14,6 +21,8 @@ const UPDATE_SIGNAL = {}; // for react-spring capability (Konva.Node.prototype as any)._applyProps = applyNodeProps; +let currentUpdatePriority: number = NoEventPriority; + export function appendInitialChild(parentInstance, child) { if (typeof child === 'string') { // Noop for string children of Text (eg foo) @@ -184,7 +193,6 @@ export function commitMount(instance, type, newProps) { export function commitUpdate( instance, - updatePayload, type, oldProps, newProps @@ -217,4 +225,50 @@ export function clearContainer(container) { export function detachDeletedInstance() {} -export const getCurrentEventPriority = () => DefaultEventPriority; +export function getCurrentEventPriority() { + return currentUpdatePriority; +} + +export function prepareScopeUpdate() {} + +export function getInstanceFromScope() { + return null; +} + +export function setCurrentUpdatePriority(newPriority) { + currentUpdatePriority = newPriority; +} + +export function getCurrentUpdatePriority() { + return currentUpdatePriority; +} + +export function resolveUpdatePriority() { + return currentUpdatePriority || DefaultEventPriority; +} + +export function shouldAttemptEagerTransition() { + return false; +} + +export function requestPostPaintCallback() {} + +export function maySuspendCommit() { + return false; +} + +export function preloadInstance() { + return true; +} + +export function startSuspendingCommit() {} + +export function suspendInstance() {} + +export function waitForCommitToBeReady() { + return null; +} + +export const NotPendingTransition = null; + +export function resetFormInstance() {} diff --git a/test/react-konva-test.tsx b/test/react-konva-test.tsx index bf1aa4a..19bc0b6 100644 --- a/test/react-konva-test.tsx +++ b/test/react-konva-test.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { unstable_batchedUpdates as batchedUpdates } from 'react-dom'; import sinon from 'sinon'; import { expect } from 'chai'; import Konva from 'konva'; @@ -17,26 +16,22 @@ import { Image, } from '../src/ReactKonva'; +global.IS_REACT_ACT_ENVIRONMENT = true; + const render = async (component) => { const node = document.createElement('div'); document.body.appendChild(node); const root = createRoot(node); - const App = ({ onUpdate, children }) => { - React.useEffect(() => { - onUpdate(null); - }); - return children; - }; - await new Promise((resolve) => { - root.render({component}); + await React.act(() => { + root.render(component); }); return { stage: Konva.stages[Konva.stages.length - 1], rerender: async (component) => { - await new Promise((resolve) => { - root.render({component}); + await React.act(() => { + root.render(component); }); }, }; @@ -68,16 +63,14 @@ describe('initial mounting and refs', () => { }); it('check all refs', async () => { - const App = () => { - const stageRef = React.useRef(null); - const layerRef = React.useRef(null); - const rectRef = React.useRef(null); + let stageRef; + let layerRef; + let rectRef; - React.useEffect(() => { - expect(stageRef.current instanceof Konva.Stage).to.be.true; - expect(layerRef.current instanceof Konva.Layer).to.be.true; - expect(rectRef.current instanceof Konva.Rect).to.be.true; - }); + const App = () => { + stageRef = React.useRef(null); + layerRef = React.useRef(null); + rectRef = React.useRef(null); return ( @@ -88,6 +81,9 @@ describe('initial mounting and refs', () => { ); }; await render(React.createElement(App)); + expect(stageRef.current instanceof Konva.Stage).to.be.true; + expect(layerRef.current instanceof Konva.Layer).to.be.true; + expect(rectRef.current instanceof Konva.Rect).to.be.true; }); it('no fail on no ref', async () => { @@ -133,7 +129,7 @@ describe('initial mounting and refs', () => { expect((ref.current as any) instanceof Konva.Rect).to.be.true; }); return ( - + @@ -188,7 +184,9 @@ describe('Test stage component', async function () { } const { stage } = await render(); - stage.simulateMouseDown({ x: 50, y: 50 }); + await React.act(() => { + stage.simulateMouseDown({ x: 50, y: 50 }); + }); expect(eventCount).to.equal(1); }); @@ -226,7 +224,9 @@ describe('Test stage component', async function () { } const { stage } = await render(); - stage.simulateMouseDown({ x: 50, y: 50 }); + await React.act(() => { + stage.simulateMouseDown({ x: 50, y: 50 }); + }); }); it('check div props', async function () { @@ -281,10 +281,6 @@ describe('Test props setting', async function () { }; await setProps({ rectProps: props1 }); - await new Promise((resolve) => { - setTimeout(resolve, 100); - }); - expect(rect.width()).to.equal(100); const props2 = { @@ -611,6 +607,11 @@ describe('Check id saving', () => { }); describe('Test drawing calls', () => { + afterEach(() => { + Konva.Layer.prototype.batchDraw.restore && + Konva.Layer.prototype.batchDraw.restore(); + }); + it('Draw layer on mount', async function () { class App extends React.Component { render() { @@ -674,6 +675,11 @@ describe('Test drawing calls', () => { }); describe('test reconciler', () => { + afterEach(() => { + Konva.Layer.prototype.batchDraw.restore && + Konva.Layer.prototype.batchDraw.restore(); + }); + it('add before', async function () { class App extends React.Component { render() { @@ -844,10 +850,16 @@ describe('test reconciler', () => { const rect1 = layer.findOne('.rect1'); - layer.getStage().simulateMouseDown({ x: 5, y: 5 }); - rect1.startDrag(); - // move mouse - layer.getStage().simulateMouseMove({ x: 10, y: 10 }); + await React.act(() => { + layer.getStage().simulateMouseDown({ x: 5, y: 5 }); + }); + await React.act(() => { + rect1.startDrag(); + }); + await React.act(() => { + // move mouse + layer.getStage().simulateMouseMove({ x: 10, y: 10 }); + }); expect(rect1.isDragging()).to.equal(true); @@ -885,9 +897,14 @@ describe('test reconciler', () => { const { stage } = await render(); expect(stage.findOne('Rect').fill()).to.equal('black'); - stage.simulateMouseDown({ x: 50, y: 50 }); - stage.simulateMouseMove({ x: 55, y: 55 }); - await new Promise((resolve) => setTimeout(resolve, 50)); + + await React.act(() => { + stage.simulateMouseDown({ x: 50, y: 50 }); + }); + await React.act(() => { + stage.simulateMouseMove({ x: 55, y: 55 }); + }); + window.stage = stage; expect(stage.findOne('Rect').isDragging()).to.equal(true); expect(stage.findOne('Rect').fill()).to.equal('red'); }); @@ -970,13 +987,10 @@ describe('Test nested context API', async function () { // wait for react team response describe('try lazy and suspense', async function () { it('can use lazy and suspense', async function () { + let resolvePromise; const LazyRect = React.lazy(() => { return new Promise((resolve) => { - setTimeout(() => { - resolve({ - default: () => , - }); - }, 5); + resolvePromise = resolve; }); }); @@ -997,20 +1011,21 @@ describe('try lazy and suspense', async function () { expect(stage.find('Text').length).to.equal(1); expect(stage.find('Shape').length).to.equal(1); - await new Promise((resolve) => setTimeout(resolve, 50)); + await React.act(() => { + resolvePromise({ + default: () => , + }); + }); expect(stage.find('Text').length).to.equal(0); expect(stage.find('Rect').length).to.equal(1); expect(stage.find('Shape').length).to.equal(1); }); it('suspends whole stage', async () => { + let promiseResolve; const LazyDiv = React.lazy(() => { return new Promise((resolve) => { - setTimeout(() => { - resolve({ - default: () =>
, - }); - }, 500); + promiseResolve = resolve; }); }); @@ -1042,7 +1057,9 @@ describe('try lazy and suspense', async function () { // then show lazy await rerender(); // wait till lazy component is loaded - await new Promise((resolve) => setTimeout(resolve, 550)); + await React.act(() => { + promiseResolve({ default: () =>
}); + }); let lastStage = Konva.stages[Konva.stages.length - 1]; // make sure all properties are set correctly expect(lastStage).to.not.equal(stage); @@ -1117,8 +1134,9 @@ describe('Hooks', async function () { const { stage } = await render(); expect(stage.findOne('Rect').fill()).to.equal('black'); - stage.simulateMouseDown({ x: 50, y: 50 }); - await new Promise((resolve) => setTimeout(resolve, 50)); + await React.act(() => { + stage.simulateMouseDown({ x: 50, y: 50 }); + }); expect(stage.findOne('Rect').fill()).to.equal('red'); }); @@ -1137,14 +1155,10 @@ describe('Hooks', async function () { }; const { stage, rerender } = await render(); - // not sure why timeouts are required - // are hooks async? - await new Promise((resolve) => setTimeout(resolve, 50)); expect(callCount).to.equal(1); rerender(); - await new Promise((resolve) => setTimeout(resolve, 50)); expect(callCount).to.equal(2); }); @@ -1173,9 +1187,6 @@ describe('Hooks', async function () { }; const { stage } = await render(); - // not sure why timeouts are required - // are hooks async? - await new Promise((resolve) => setTimeout(resolve, 50)); const rect = stage.findOne('Rect'); expect(rect.name()).to.equal('rect name'); @@ -1329,7 +1340,6 @@ describe('update order', () => { it.skip('update order', async function () { const { stage } = await render(); - await new Promise((resolve) => setTimeout(resolve, 150)); await store.dispatch(); expect(renderCallStack).to.deep.equal([ 'ViewLayer',