UNPKG

react-konva

Version:

React binding to canvas element via Konva framework

180 lines (179 loc) 7.13 kB
/** * Based on ReactArt.js * Copyright (c) 2017-present Lavrenov Anton. * All rights reserved. * * MIT */ 'use strict'; 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 };