diff --git a/excalidraw-app/collab/Collab.tsx b/excalidraw-app/collab/Collab.tsx index 9b26af054e..92d94dbc9e 100644 --- a/excalidraw-app/collab/Collab.tsx +++ b/excalidraw-app/collab/Collab.tsx @@ -22,7 +22,6 @@ import { preventUnload, resolvablePromise, throttleRAF, - withBatchedUpdates, } from "../../packages/excalidraw/utils"; import { CURSOR_SYNC_TIMEOUT, @@ -83,6 +82,7 @@ import { atom, useAtom } from "jotai"; import { appJotaiStore } from "../app-jotai"; import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types"; import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds"; +import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils"; export const collabAPIAtom = atom(null); export const collabDialogShownAtom = atom(false); diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 5b0a3b593f..792db29f78 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -259,9 +259,7 @@ import { sceneCoordsToViewportCoords, tupleToCoors, viewportCoordsToSceneCoords, - withBatchedUpdates, wrapEvent, - withBatchedUpdatesThrottled, updateObject, updateActiveTool, getShortcutKey, @@ -403,6 +401,7 @@ import { ElementCanvasButton } from "./MagicButton"; import { MagicIcon, copyIcon, fullscreenIcon } from "./icons"; import { EditorLocalStorage } from "../data/EditorLocalStorage"; import FollowMode from "./FollowMode/FollowMode"; +import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils"; const AppContext = React.createContext(null!); const AppPropsContext = React.createContext(null!); diff --git a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx index 5a524921a2..0aaa52c7ce 100644 --- a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx +++ b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx @@ -1,10 +1,6 @@ import React, { useEffect, useRef } from "react"; import { renderInteractiveScene } from "../../renderer/renderScene"; -import { - isRenderThrottlingEnabled, - isShallowEqual, - sceneCoordsToViewportCoords, -} from "../../utils"; +import { isShallowEqual, sceneCoordsToViewportCoords } from "../../utils"; import { CURSOR_TYPE } from "../../constants"; import { t } from "../../i18n"; import type { DOMAttributes } from "react"; @@ -14,6 +10,7 @@ import type { RenderInteractiveSceneCallback, } from "../../scene/types"; import type { NonDeletedExcalidrawElement } from "../../element/types"; +import { isRenderThrottlingEnabled } from "../../reactUtils"; type InteractiveCanvasProps = { containerRef: React.RefObject; diff --git a/packages/excalidraw/components/canvases/StaticCanvas.tsx b/packages/excalidraw/components/canvases/StaticCanvas.tsx index 38b9baade5..c8174566bb 100644 --- a/packages/excalidraw/components/canvases/StaticCanvas.tsx +++ b/packages/excalidraw/components/canvases/StaticCanvas.tsx @@ -1,10 +1,11 @@ import React, { useEffect, useRef } from "react"; import { RoughCanvas } from "roughjs/bin/canvas"; import { renderStaticScene } from "../../renderer/renderScene"; -import { isRenderThrottlingEnabled, isShallowEqual } from "../../utils"; +import { isShallowEqual } from "../../utils"; import type { AppState, StaticCanvasAppState } from "../../types"; import type { StaticCanvasRenderConfig } from "../../scene/types"; import type { NonDeletedExcalidrawElement } from "../../element/types"; +import { isRenderThrottlingEnabled } from "../../reactUtils"; type StaticCanvasProps = { canvas: HTMLCanvasElement; diff --git a/packages/excalidraw/example/App.tsx b/packages/excalidraw/example/App.tsx index 4a51607881..50dc5b9a3b 100644 --- a/packages/excalidraw/example/App.tsx +++ b/packages/excalidraw/example/App.tsx @@ -5,12 +5,7 @@ import type * as TExcalidraw from "../index"; import "./App.scss"; import initialData from "./initialData"; import { nanoid } from "nanoid"; -import { - resolvablePromise, - ResolvablePromise, - withBatchedUpdates, - withBatchedUpdatesThrottled, -} from "../utils"; +import { resolvablePromise, ResolvablePromise } from "../utils"; import { EVENT, ROUNDNESS } from "../constants"; import { distance2d } from "../math"; import { fileOpen } from "../data/filesystem"; @@ -29,6 +24,7 @@ import { ImportedLibraryData } from "../data/types"; import CustomFooter from "./CustomFooter"; import MobileFooter from "./MobileFooter"; import { KEYS } from "../keys"; +import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils"; declare global { interface Window { diff --git a/packages/excalidraw/reactUtils.ts b/packages/excalidraw/reactUtils.ts new file mode 100644 index 0000000000..535302d42f --- /dev/null +++ b/packages/excalidraw/reactUtils.ts @@ -0,0 +1,61 @@ +/** + * @param func handler taking at most single parameter (event). + */ + +import { unstable_batchedUpdates } from "react-dom"; +import { version as ReactVersion } from "react"; +import { throttleRAF } from "./utils"; + +export const withBatchedUpdates = < + TFunction extends ((event: any) => void) | (() => void), +>( + func: Parameters["length"] extends 0 | 1 ? TFunction : never, +) => + ((event) => { + unstable_batchedUpdates(func as TFunction, event); + }) as TFunction; + +/** + * barches React state updates and throttles the calls to a single call per + * animation frame + */ +export const withBatchedUpdatesThrottled = < + TFunction extends ((event: any) => void) | (() => void), +>( + func: Parameters["length"] extends 0 | 1 ? TFunction : never, +) => { + // @ts-ignore + return throttleRAF>(((event) => { + unstable_batchedUpdates(func, event); + }) as TFunction); +}; + +export const isRenderThrottlingEnabled = (() => { + // we don't want to throttle in react < 18 because of #5439 and it was + // getting more complex to maintain the fix + let IS_REACT_18_AND_UP: boolean; + try { + const version = ReactVersion.split("."); + IS_REACT_18_AND_UP = Number(version[0]) > 17; + } catch { + IS_REACT_18_AND_UP = false; + } + + let hasWarned = false; + + return () => { + if (window.EXCALIDRAW_THROTTLE_RENDER === true) { + if (!IS_REACT_18_AND_UP) { + if (!hasWarned) { + hasWarned = true; + console.warn( + "Excalidraw: render throttling is disabled on React versions < 18.", + ); + } + return false; + } + return true; + } + return false; + }; +})(); diff --git a/packages/excalidraw/utils.ts b/packages/excalidraw/utils.ts index 1f0e317608..c2afedb32f 100644 --- a/packages/excalidraw/utils.ts +++ b/packages/excalidraw/utils.ts @@ -14,9 +14,7 @@ import { UnsubscribeCallback, Zoom, } from "./types"; -import { unstable_batchedUpdates } from "react-dom"; import { ResolutionType } from "./utility-types"; -import React from "react"; let mockDateTime: string | null = null; @@ -555,33 +553,6 @@ export const resolvablePromise = () => { return promise as ResolvablePromise; }; -/** - * @param func handler taking at most single parameter (event). - */ -export const withBatchedUpdates = < - TFunction extends ((event: any) => void) | (() => void), ->( - func: Parameters["length"] extends 0 | 1 ? TFunction : never, -) => - ((event) => { - unstable_batchedUpdates(func as TFunction, event); - }) as TFunction; - -/** - * barches React state updates and throttles the calls to a single call per - * animation frame - */ -export const withBatchedUpdatesThrottled = < - TFunction extends ((event: any) => void) | (() => void), ->( - func: Parameters["length"] extends 0 | 1 ? TFunction : never, -) => { - // @ts-ignore - return throttleRAF>(((event) => { - unstable_batchedUpdates(func, event); - }) as TFunction); -}; - //https://stackoverflow.com/a/9462382/8418 export const nFormatter = (num: number, digits: number): string => { const si = [ @@ -939,36 +910,6 @@ export const memoize = , R extends any>( return ret as typeof func & { clear: () => void }; }; -export const isRenderThrottlingEnabled = (() => { - // we don't want to throttle in react < 18 because of #5439 and it was - // getting more complex to maintain the fix - let IS_REACT_18_AND_UP: boolean; - try { - const version = React.version.split("."); - IS_REACT_18_AND_UP = Number(version[0]) > 17; - } catch { - IS_REACT_18_AND_UP = false; - } - - let hasWarned = false; - - return () => { - if (window.EXCALIDRAW_THROTTLE_RENDER === true) { - if (!IS_REACT_18_AND_UP) { - if (!hasWarned) { - hasWarned = true; - console.warn( - "Excalidraw: render throttling is disabled on React versions < 18.", - ); - } - return false; - } - return true; - } - return false; - }; -})(); - /** Checks if value is inside given collection. Useful for type-safety. */ export const isMemberOf = ( /** Set/Map/Array/Object */