react-konva
Version:
React binding to canvas element via Konva framework
180 lines (179 loc) • 7.13 kB
JavaScript
/**
* Based on ReactArt.js
* Copyright (c) 2017-present Lavrenov Anton.
* All rights reserved.
*
* MIT
*/
;
import React from 'react';
if (React.version.indexOf('19') === -1) {
throw new Error('react-konva version 19 is only compatible with React 19. Make sure to have the last version of react-konva and react or downgrade react-konva to version 18.');
}
import Konva from 'konva/lib/Core.js';
import ReactFiberReconciler from 'react-reconciler';
import { ConcurrentRoot } from 'react-reconciler/constants.js';
import * as HostConfig from './ReactKonvaHostConfig.js';
import { applyNodeProps, toggleStrictMode } from './makeUpdates.js';
import { useContextBridge, FiberProvider } from 'its-fine';
function usePrevious(value) {
const ref = React.useRef({});
React.useLayoutEffect(() => {
ref.current = value;
});
React.useLayoutEffect(() => {
return () => {
// when using suspense it is possible that stage is unmounted
// but React still keep component ref
// in that case we need to manually flush props
// we have a special test for that
ref.current = {};
};
}, []);
return ref.current;
}
const useIsReactStrictMode = () => {
const memoCount = React.useRef(0);
// in strict mode, memo will be called twice
React.useMemo(() => {
memoCount.current++;
}, []);
return memoCount.current > 1;
};
const StageWrap = (props) => {
const container = React.useRef(null);
const stage = React.useRef(null);
const fiberRef = React.useRef(null);
const oldProps = usePrevious(props);
const Bridge = useContextBridge();
const pendingDestroy = React.useRef(null);
const _setRef = (stage) => {
const { forwardedRef } = props;
if (!forwardedRef) {
return;
}
if (typeof forwardedRef === 'function') {
forwardedRef(stage);
}
else {
forwardedRef.current = stage;
}
};
const isStrictMode = useIsReactStrictMode();
const destroyStage = () => {
_setRef(null);
KonvaRenderer.updateContainer(null, fiberRef.current, null);
stage.current?.destroy();
stage.current = null;
};
React.useLayoutEffect(() => {
// Cancel any pending destruction (happens during re-ordering in strict mode)
if (pendingDestroy.current) {
clearTimeout(pendingDestroy.current);
pendingDestroy.current = null;
}
// If stage already exists (re-ordering scenario), reuse it
if (stage.current) {
_setRef(stage.current);
}
else {
stage.current = new Konva.Stage({
width: props.width,
height: props.height,
container: container.current,
});
_setRef(stage.current);
// @ts-ignore
fiberRef.current = KonvaRenderer.createContainer(stage.current, ConcurrentRoot, null, false, null, '', console.error, console.error, console.error, null);
KonvaRenderer.updateContainer(React.createElement(Bridge, {}, props.children), fiberRef.current, null, () => { });
}
return () => {
if (isStrictMode) {
// Delay destruction to allow cancellation on remount
pendingDestroy.current = setTimeout(destroyStage, 0);
}
else {
destroyStage();
}
};
}, []);
React.useLayoutEffect(() => {
_setRef(stage.current);
applyNodeProps(stage.current, props, oldProps);
// =============================================================================
// CRITICAL FIX - DO NOT REMOVE
// =============================================================================
// This flushSyncFromReconciler wrapper is CRITICAL for React 19 compatibility.
//
// THE BUG:
// When using useSyncExternalStore (MobX, Zustand, etc.) with react-konva,
// parent component's useLayoutEffect couldn't find newly added Konva nodes.
// The nodes were added to the store, but not yet rendered to the canvas.
//
// ROOT CAUSE:
// React 19's updateContainer can defer Konva reconciler work to a later
// microtask. Combined with Bridge component (from its-fine) and Html components
// that create secondary React roots, this caused child components to render
// AFTER parent's useLayoutEffect completed.
//
// THE FIX:
// flushSyncFromReconciler forces ALL scheduled Konva reconciler work to
// complete synchronously within this useLayoutEffect, ensuring child
// components render before any parent effects run.
//
// WARNING - NOT COVERED BY TESTS:
// This bug CANNOT be reproduced in local test environments (Vitest/Playwright).
// It only manifests in production builds with specific conditions:
// - MobX/useSyncExternalStore for state management
// - Html components with secondary React roots using Bridge
// - Complex component hierarchies (multiple pages/stages)
// The fix was verified in Polotno production app.
// =============================================================================
KonvaRenderer.flushSyncFromReconciler(() => {
KonvaRenderer.updateContainer(React.createElement(Bridge, {}, props.children), fiberRef.current, null);
});
});
return React.createElement('div', {
ref: container,
id: props.id,
accessKey: props.accessKey,
className: props.className,
role: props.role,
style: props.style,
tabIndex: props.tabIndex,
title: props.title,
});
};
export const Layer = 'Layer';
export const FastLayer = 'FastLayer';
export const Group = 'Group';
export const Label = 'Label';
export const Rect = 'Rect';
export const Circle = 'Circle';
export const Ellipse = 'Ellipse';
export const Wedge = 'Wedge';
export const Line = 'Line';
export const Sprite = 'Sprite';
export const Image = 'Image';
export const Text = 'Text';
export const TextPath = 'TextPath';
export const Star = 'Star';
export const Ring = 'Ring';
export const Arc = 'Arc';
export const Tag = 'Tag';
export const Path = 'Path';
export const RegularPolygon = 'RegularPolygon';
export const Arrow = 'Arrow';
export const Shape = 'Shape';
export const Transformer = 'Transformer';
export const version = '19.2.2';
// @ts-ignore
export const KonvaRenderer = ReactFiberReconciler(HostConfig);
// Update Stage component declaration
export const Stage = React.forwardRef((props, ref) => {
return React.createElement(FiberProvider, {}, React.createElement(StageWrap, { ...props, forwardedRef: ref }));
});
export const useStrictMode = toggleStrictMode;
// export useContextBridge from its-fine for reuse in react-konva-utils
// so react-konva-utils don't use its own version of its-fine (it is possible on pnpm)
export { useContextBridge };