Ver código fonte

perf: use `UIAppState` where possible to reduce UI rerenders (#6560)

David Luzar 2 anos atrás
pai
commit
560231d365
33 arquivos alterados com 155 adições e 125 exclusões
  1. 7 6
      src/components/Actions.tsx
  2. 4 4
      src/components/HintViewer.tsx
  3. 6 5
      src/components/ImageExportDialog.tsx
  4. 4 4
      src/components/JSONExportDialog.tsx
  5. 25 15
      src/components/LayerUI.tsx
  6. 8 3
      src/components/LibraryMenu.tsx
  7. 2 2
      src/components/LibraryMenuBrowseButton.tsx
  8. 2 2
      src/components/LibraryMenuControlButtons.tsx
  9. 6 5
      src/components/LibraryMenuHeaderContent.tsx
  10. 9 4
      src/components/LibraryMenuItems.tsx
  11. 5 5
      src/components/MobileMenu.tsx
  12. 4 3
      src/components/PasteChartDialog.tsx
  13. 3 3
      src/components/PublishLibrary.tsx
  14. 3 6
      src/components/Sidebar/Sidebar.tsx
  15. 4 4
      src/components/Sidebar/SidebarTrigger.tsx
  16. 3 3
      src/components/Stats.tsx
  17. 3 2
      src/components/dropdownMenu/DropdownMenuTrigger.tsx
  18. 2 2
      src/components/footer/Footer.tsx
  19. 2 2
      src/components/footer/FooterCenter.tsx
  20. 2 2
      src/components/live-collaboration/LiveCollaborationTrigger.tsx
  21. 4 7
      src/components/main-menu/DefaultItems.tsx
  22. 3 6
      src/components/main-menu/MainMenu.tsx
  23. 3 6
      src/components/welcome-screen/WelcomeScreen.Center.tsx
  24. 2 2
      src/context/ui-appState.ts
  25. 3 2
      src/element/Hyperlink.tsx
  26. 2 2
      src/element/showSelectedShapeActions.ts
  27. 3 2
      src/excalidraw-app/CustomStats.tsx
  28. 2 2
      src/excalidraw-app/components/ExportToExcalidrawPlus.tsx
  29. 1 1
      src/excalidraw-app/data/index.ts
  30. 3 2
      src/excalidraw-app/index.tsx
  31. 4 4
      src/scene/selection.ts
  32. 14 5
      src/types.ts
  33. 7 2
      src/utils.ts

+ 7 - 6
src/components/Actions.tsx

@@ -14,7 +14,7 @@ import {
   hasText,
 } from "../scene";
 import { SHAPES } from "../shapes";
-import { AppState, Zoom } from "../types";
+import { UIAppState, Zoom } from "../types";
 import {
   capitalizeString,
   isTransparent,
@@ -28,19 +28,20 @@ import { trackEvent } from "../analytics";
 import { hasBoundTextElement } from "../element/typeChecks";
 import clsx from "clsx";
 import { actionToggleZenMode } from "../actions";
-import "./Actions.scss";
 import { Tooltip } from "./Tooltip";
 import {
   shouldAllowVerticalAlign,
   suppportsHorizontalAlign,
 } from "../element/textElement";
 
+import "./Actions.scss";
+
 export const SelectedShapeActions = ({
   appState,
   elements,
   renderAction,
 }: {
-  appState: AppState;
+  appState: UIAppState;
   elements: readonly ExcalidrawElement[];
   renderAction: ActionManager["renderAction"];
 }) => {
@@ -215,10 +216,10 @@ export const ShapesSwitcher = ({
   appState,
 }: {
   canvas: HTMLCanvasElement | null;
-  activeTool: AppState["activeTool"];
-  setAppState: React.Component<any, AppState>["setState"];
+  activeTool: UIAppState["activeTool"];
+  setAppState: React.Component<any, UIAppState>["setState"];
   onImageAction: (data: { pointerType: PointerType | null }) => void;
-  appState: AppState;
+  appState: UIAppState;
 }) => (
   <>
     {SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {

+ 4 - 4
src/components/HintViewer.tsx

@@ -1,9 +1,7 @@
 import { t } from "../i18n";
 import { NonDeletedExcalidrawElement } from "../element/types";
 import { getSelectedElements } from "../scene";
-
-import "./HintViewer.scss";
-import { AppState, Device } from "../types";
+import { Device, UIAppState } from "../types";
 import {
   isImageElement,
   isLinearElement,
@@ -13,8 +11,10 @@ import {
 import { getShortcutKey } from "../utils";
 import { isEraserActive } from "../appState";
 
+import "./HintViewer.scss";
+
 interface HintViewerProps {
-  appState: AppState;
+  appState: UIAppState;
   elements: readonly NonDeletedExcalidrawElement[];
   isMobile: boolean;
   device: Device;

+ 6 - 5
src/components/ImageExportDialog.tsx

@@ -4,11 +4,10 @@ import { canvasToBlob } from "../data/blob";
 import { NonDeletedExcalidrawElement } from "../element/types";
 import { t } from "../i18n";
 import { getSelectedElements, isSomeElementSelected } from "../scene";
-import { AppState, BinaryFiles } from "../types";
+import { BinaryFiles, UIAppState } from "../types";
 import { Dialog } from "./Dialog";
 import { clipboard } from "./icons";
 import Stack from "./Stack";
-import "./ExportDialog.scss";
 import OpenColor from "open-color";
 import { CheckboxItem } from "./CheckboxItem";
 import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants";
@@ -16,6 +15,8 @@ import { nativeFileSystemSupported } from "../data/filesystem";
 import { ActionManager } from "../actions/manager";
 import { exportToCanvas } from "../packages/utils";
 
+import "./ExportDialog.scss";
+
 const supportsContextFilters =
   "filter" in document.createElement("canvas").getContext("2d")!;
 
@@ -70,7 +71,7 @@ const ImageExportModal = ({
   onExportToSvg,
   onExportToClipboard,
 }: {
-  appState: AppState;
+  appState: UIAppState;
   elements: readonly NonDeletedExcalidrawElement[];
   files: BinaryFiles;
   exportPadding?: number;
@@ -216,8 +217,8 @@ export const ImageExportDialog = ({
   onExportToSvg,
   onExportToClipboard,
 }: {
-  appState: AppState;
-  setAppState: React.Component<any, AppState>["setState"];
+  appState: UIAppState;
+  setAppState: React.Component<any, UIAppState>["setState"];
   elements: readonly NonDeletedExcalidrawElement[];
   files: BinaryFiles;
   exportPadding?: number;

+ 4 - 4
src/components/JSONExportDialog.tsx

@@ -2,7 +2,7 @@ import React from "react";
 import { NonDeletedExcalidrawElement } from "../element/types";
 import { t } from "../i18n";
 
-import { AppState, ExportOpts, BinaryFiles } from "../types";
+import { ExportOpts, BinaryFiles, UIAppState } from "../types";
 import { Dialog } from "./Dialog";
 import { exportToFileIcon, LinkIcon } from "./icons";
 import { ToolButton } from "./ToolButton";
@@ -28,7 +28,7 @@ const JSONExportModal = ({
   exportOpts,
   canvas,
 }: {
-  appState: AppState;
+  appState: UIAppState;
   files: BinaryFiles;
   elements: readonly NonDeletedExcalidrawElement[];
   actionManager: ActionManager;
@@ -96,12 +96,12 @@ export const JSONExportDialog = ({
   setAppState,
 }: {
   elements: readonly NonDeletedExcalidrawElement[];
-  appState: AppState;
+  appState: UIAppState;
   files: BinaryFiles;
   actionManager: ActionManager;
   exportOpts: ExportOpts;
   canvas: HTMLCanvasElement | null;
-  setAppState: React.Component<any, AppState>["setState"];
+  setAppState: React.Component<any, UIAppState>["setState"];
 }) => {
   const handleClose = React.useCallback(() => {
     setAppState({ openDialog: null });

+ 25 - 15
src/components/LayerUI.tsx

@@ -8,7 +8,13 @@ import { NonDeletedExcalidrawElement } from "../element/types";
 import { Language, t } from "../i18n";
 import { calculateScrollCenter } from "../scene";
 import { ExportType } from "../scene/types";
-import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
+import {
+  AppProps,
+  AppState,
+  ExcalidrawProps,
+  BinaryFiles,
+  UIAppState,
+} from "../types";
 import { capitalizeString, isShallowEqual, muteFSAbortError } from "../utils";
 import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
 import { ErrorDialog } from "./ErrorDialog";
@@ -49,7 +55,7 @@ import "./Toolbar.scss";
 
 interface LayerUIProps {
   actionManager: ActionManager;
-  appState: AppState;
+  appState: UIAppState;
   files: BinaryFiles;
   canvas: HTMLCanvasElement | null;
   setAppState: React.Component<any, AppState>["setState"];
@@ -144,7 +150,8 @@ const LayerUI = ({
         const fileHandle = await exportCanvas(
           type,
           exportedElements,
-          appState,
+          // FIXME once we split UI canvas from element canvas
+          appState as AppState,
           files,
           {
             exportBackground: appState.exportBackground,
@@ -458,9 +465,9 @@ const LayerUI = ({
               <button
                 className="scroll-back-to-content"
                 onClick={() => {
-                  setAppState({
+                  setAppState((appState) => ({
                     ...calculateScrollCenter(elements, appState, canvas),
-                  });
+                  }));
                 }}
               >
                 {t("buttons.scrollBackToContent")}
@@ -484,14 +491,15 @@ const LayerUI = ({
   );
 };
 
-const stripIrrelevantAppStateProps = (
-  appState: AppState,
-): Omit<
-  AppState,
-  "suggestedBindings" | "startBoundElement" | "cursorButton"
-> => {
-  const { suggestedBindings, startBoundElement, cursorButton, ...ret } =
-    appState;
+const stripIrrelevantAppStateProps = (appState: AppState): UIAppState => {
+  const {
+    suggestedBindings,
+    startBoundElement,
+    cursorButton,
+    scrollX,
+    scrollY,
+    ...ret
+  } = appState;
   return ret;
 };
 
@@ -506,8 +514,10 @@ const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
 
   return (
     isShallowEqual(
-      stripIrrelevantAppStateProps(prevAppState),
-      stripIrrelevantAppStateProps(nextAppState),
+      // asserting AppState because we're being passed the whole AppState
+      // but resolve to only the UI-relevant props
+      stripIrrelevantAppStateProps(prevAppState as AppState),
+      stripIrrelevantAppStateProps(nextAppState as AppState),
       {
         selectedElementIds: isShallowEqual,
         selectedGroupIds: isShallowEqual,

+ 8 - 3
src/components/LibraryMenu.tsx

@@ -5,7 +5,12 @@ import Library, {
 } from "../data/library";
 import { t } from "../i18n";
 import { randomId } from "../random";
-import { LibraryItems, LibraryItem, AppState, ExcalidrawProps } from "../types";
+import {
+  LibraryItems,
+  LibraryItem,
+  ExcalidrawProps,
+  UIAppState,
+} from "../types";
 import LibraryMenuItems from "./LibraryMenuItems";
 import { trackEvent } from "../analytics";
 import { atom, useAtom } from "jotai";
@@ -44,11 +49,11 @@ export const LibraryMenuContent = ({
   pendingElements: LibraryItem["elements"];
   onInsertLibraryItems: (libraryItems: LibraryItems) => void;
   onAddToLibrary: () => void;
-  setAppState: React.Component<any, AppState>["setState"];
+  setAppState: React.Component<any, UIAppState>["setState"];
   libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
   library: Library;
   id: string;
-  appState: AppState;
+  appState: UIAppState;
   selectedItems: LibraryItem["id"][];
   onSelectItems: (id: LibraryItem["id"][]) => void;
 }) => {

+ 2 - 2
src/components/LibraryMenuBrowseButton.tsx

@@ -1,6 +1,6 @@
 import { VERSIONS } from "../constants";
 import { t } from "../i18n";
-import { AppState, ExcalidrawProps } from "../types";
+import { ExcalidrawProps, UIAppState } from "../types";
 
 const LibraryMenuBrowseButton = ({
   theme,
@@ -8,7 +8,7 @@ const LibraryMenuBrowseButton = ({
   libraryReturnUrl,
 }: {
   libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
-  theme: AppState["theme"];
+  theme: UIAppState["theme"];
   id: string;
 }) => {
   const referrer =

+ 2 - 2
src/components/LibraryMenuControlButtons.tsx

@@ -1,4 +1,4 @@
-import { LibraryItem, ExcalidrawProps, AppState } from "../types";
+import { LibraryItem, ExcalidrawProps, UIAppState } from "../types";
 import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
 import { LibraryDropdownMenu } from "./LibraryMenuHeaderContent";
 
@@ -13,7 +13,7 @@ export const LibraryMenuControlButtons = ({
   selectedItems: LibraryItem["id"][];
   onSelectItems: (id: LibraryItem["id"][]) => void;
   libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
-  theme: AppState["theme"];
+  theme: UIAppState["theme"];
   id: string;
   style: React.CSSProperties;
 }) => {

+ 6 - 5
src/components/LibraryMenuHeaderContent.tsx

@@ -1,8 +1,8 @@
 import { useCallback, useState } from "react";
 import { t } from "../i18n";
 import { jotaiScope } from "../jotai";
-import { AppState, LibraryItem, LibraryItems } from "../types";
-import { useApp, useExcalidrawAppState, useExcalidrawSetAppState } from "./App";
+import { LibraryItem, LibraryItems, UIAppState } from "../types";
+import { useApp, useExcalidrawSetAppState } from "./App";
 import { saveLibraryAsJSON } from "../data/json";
 import Library, { libraryItemsAtom } from "../data/library";
 import {
@@ -21,6 +21,7 @@ import PublishLibrary from "./PublishLibrary";
 import { Dialog } from "./Dialog";
 import DropdownMenu from "./dropdownMenu/DropdownMenu";
 import { isLibraryMenuOpenAtom } from "./LibraryMenu";
+import { useUIAppState } from "../context/ui-appState";
 
 const getSelectedItems = (
   libraryItems: LibraryItems,
@@ -28,13 +29,13 @@ const getSelectedItems = (
 ) => libraryItems.filter((item) => selectedItems.includes(item.id));
 
 export const LibraryDropdownMenuButton: React.FC<{
-  setAppState: React.Component<any, AppState>["setState"];
+  setAppState: React.Component<any, UIAppState>["setState"];
   selectedItems: LibraryItem["id"][];
   library: Library;
   onRemoveFromLibrary: () => void;
   resetLibrary: () => void;
   onSelectItems: (items: LibraryItem["id"][]) => void;
-  appState: AppState;
+  appState: UIAppState;
 }> = ({
   setAppState,
   selectedItems,
@@ -270,7 +271,7 @@ export const LibraryDropdownMenu = ({
   onSelectItems: (id: LibraryItem["id"][]) => void;
 }) => {
   const { library } = useApp();
-  const appState = useExcalidrawAppState();
+  const appState = useUIAppState();
   const setAppState = useExcalidrawSetAppState();
 
   const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);

+ 9 - 4
src/components/LibraryMenuItems.tsx

@@ -2,17 +2,22 @@ import React, { useState } from "react";
 import { serializeLibraryAsJSON } from "../data/json";
 import { ExcalidrawElement, NonDeleted } from "../element/types";
 import { t } from "../i18n";
-import { AppState, ExcalidrawProps, LibraryItem, LibraryItems } from "../types";
+import {
+  ExcalidrawProps,
+  LibraryItem,
+  LibraryItems,
+  UIAppState,
+} from "../types";
 import { arrayToMap, chunk } from "../utils";
 import { LibraryUnit } from "./LibraryUnit";
 import Stack from "./Stack";
-
-import "./LibraryMenuItems.scss";
 import { MIME_TYPES } from "../constants";
 import Spinner from "./Spinner";
 import { duplicateElements } from "../element/newElement";
 import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
 
+import "./LibraryMenuItems.scss";
+
 const CELLS_PER_ROW = 4;
 
 const LibraryMenuItems = ({
@@ -35,7 +40,7 @@ const LibraryMenuItems = ({
   selectedItems: LibraryItem["id"][];
   onSelectItems: (id: LibraryItem["id"][]) => void;
   libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
-  theme: AppState["theme"];
+  theme: UIAppState["theme"];
   id: string;
 }) => {
   const [lastSelectedItem, setLastSelectedItem] = useState<

+ 5 - 5
src/components/MobileMenu.tsx

@@ -1,5 +1,5 @@
 import React from "react";
-import { AppState, Device, ExcalidrawProps } from "../types";
+import { AppState, Device, ExcalidrawProps, UIAppState } from "../types";
 import { ActionManager } from "../actions/manager";
 import { t } from "../i18n";
 import Stack from "./Stack";
@@ -21,7 +21,7 @@ import { isHandToolActive } from "../appState";
 import { useTunnels } from "../context/tunnels";
 
 type MobileMenuProps = {
-  appState: AppState;
+  appState: UIAppState;
   actionManager: ActionManager;
   renderJSONExportDialog: () => React.ReactNode;
   renderImageExportDialog: () => React.ReactNode;
@@ -35,7 +35,7 @@ type MobileMenuProps = {
   onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
   renderTopRightUI?: (
     isMobile: boolean,
-    appState: AppState,
+    appState: UIAppState,
   ) => JSX.Element | null;
   renderCustomStats?: ExcalidrawProps["renderCustomStats"];
   renderSidebars: () => JSX.Element | null;
@@ -193,9 +193,9 @@ export const MobileMenu = ({
                 <button
                   className="scroll-back-to-content"
                   onClick={() => {
-                    setAppState({
+                    setAppState((appState) => ({
                       ...calculateScrollCenter(elements, appState, canvas),
-                    });
+                    }));
                   }}
                 >
                   {t("buttons.scrollBackToContent")}

+ 4 - 3
src/components/PasteChartDialog.tsx

@@ -5,9 +5,10 @@ import { ChartElements, renderSpreadsheet, Spreadsheet } from "../charts";
 import { ChartType } from "../element/types";
 import { t } from "../i18n";
 import { exportToSvg } from "../scene/export";
-import { AppState } from "../types";
+import { UIAppState } from "../types";
 import { useApp } from "./App";
 import { Dialog } from "./Dialog";
+
 import "./PasteChartDialog.scss";
 
 type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
@@ -80,9 +81,9 @@ export const PasteChartDialog = ({
   appState,
   onClose,
 }: {
-  appState: AppState;
+  appState: UIAppState;
   onClose: () => void;
-  setAppState: React.Component<any, AppState>["setState"];
+  setAppState: React.Component<any, UIAppState>["setState"];
 }) => {
   const { onInsertElements } = useApp();
   const handleClose = React.useCallback(() => {

+ 3 - 3
src/components/PublishLibrary.tsx

@@ -4,7 +4,7 @@ import OpenColor from "open-color";
 import { Dialog } from "./Dialog";
 import { t } from "../i18n";
 
-import { AppState, LibraryItems, LibraryItem } from "../types";
+import { LibraryItems, LibraryItem, UIAppState } from "../types";
 import { exportToCanvas, exportToSvg } from "../packages/utils";
 import {
   EXPORT_DATA_TYPES,
@@ -135,7 +135,7 @@ const SingleLibraryItem = ({
   onRemove,
 }: {
   libItem: LibraryItem;
-  appState: AppState;
+  appState: UIAppState;
   index: number;
   onChange: (val: string, index: number) => void;
   onRemove: (id: string) => void;
@@ -231,7 +231,7 @@ const PublishLibrary = ({
 }: {
   onClose: () => void;
   libraryItems: LibraryItems;
-  appState: AppState;
+  appState: UIAppState;
   onSuccess: (data: {
     url: string;
     authorName: string;

+ 3 - 6
src/components/Sidebar/Sidebar.tsx

@@ -18,11 +18,7 @@ import {
 } from "./common";
 import { SidebarHeader } from "./SidebarHeader";
 import clsx from "clsx";
-import {
-  useDevice,
-  useExcalidrawAppState,
-  useExcalidrawSetAppState,
-} from "../App";
+import { useDevice, useExcalidrawSetAppState } from "../App";
 import { updateObject } from "../../utils";
 import { KEYS } from "../../keys";
 import { EVENT } from "../../constants";
@@ -33,6 +29,7 @@ import { SidebarTabs } from "./SidebarTabs";
 import { SidebarTab } from "./SidebarTab";
 
 import "./Sidebar.scss";
+import { useUIAppState } from "../../context/ui-appState";
 
 // FIXME replace this with the implem from ColorPicker once it's merged
 const useOnClickOutside = (
@@ -185,7 +182,7 @@ SidebarInner.displayName = "SidebarInner";
 
 export const Sidebar = Object.assign(
   forwardRef((props: SidebarProps, ref: React.ForwardedRef<HTMLDivElement>) => {
-    const appState = useExcalidrawAppState();
+    const appState = useUIAppState();
 
     const { onStateChange } = props;
 

+ 4 - 4
src/components/Sidebar/SidebarTrigger.tsx

@@ -1,8 +1,9 @@
-import { useExcalidrawSetAppState, useExcalidrawAppState } from "../App";
+import { useExcalidrawSetAppState } from "../App";
 import { SidebarTriggerProps } from "./common";
+import { useUIAppState } from "../../context/ui-appState";
+import clsx from "clsx";
 
 import "./SidebarTrigger.scss";
-import clsx from "clsx";
 
 export const SidebarTrigger = ({
   name,
@@ -15,8 +16,7 @@ export const SidebarTrigger = ({
   style,
 }: SidebarTriggerProps) => {
   const setAppState = useExcalidrawSetAppState();
-  // TODO replace with sidebar context
-  const appState = useExcalidrawAppState();
+  const appState = useUIAppState();
 
   return (
     <label title={title}>

+ 3 - 3
src/components/Stats.tsx

@@ -3,14 +3,14 @@ import { getCommonBounds } from "../element/bounds";
 import { NonDeletedExcalidrawElement } from "../element/types";
 import { t } from "../i18n";
 import { getTargetElements } from "../scene";
-import { AppState, ExcalidrawProps } from "../types";
+import { ExcalidrawProps, UIAppState } from "../types";
 import { CloseIcon } from "./icons";
 import { Island } from "./Island";
 import "./Stats.scss";
 
 export const Stats = (props: {
-  appState: AppState;
-  setAppState: React.Component<any, AppState>["setState"];
+  appState: UIAppState;
+  setAppState: React.Component<any, UIAppState>["setState"];
   elements: readonly NonDeletedExcalidrawElement[];
   onClose: () => void;
   renderCustomStats: ExcalidrawProps["renderCustomStats"];

+ 3 - 2
src/components/dropdownMenu/DropdownMenuTrigger.tsx

@@ -1,5 +1,6 @@
 import clsx from "clsx";
-import { useDevice, useExcalidrawAppState } from "../App";
+import { useUIAppState } from "../../context/ui-appState";
+import { useDevice } from "../App";
 
 const MenuTrigger = ({
   className = "",
@@ -10,7 +11,7 @@ const MenuTrigger = ({
   children: React.ReactNode;
   onToggle: () => void;
 }) => {
-  const appState = useExcalidrawAppState();
+  const appState = useUIAppState();
   const device = useDevice();
   const classNames = clsx(
     `dropdown-menu-button ${className}`,

+ 2 - 2
src/components/footer/Footer.tsx

@@ -1,7 +1,6 @@
 import clsx from "clsx";
 import { actionShortcuts } from "../../actions";
 import { ActionManager } from "../../actions/manager";
-import { AppState } from "../../types";
 import {
   ExitZenModeAction,
   FinalizeAction,
@@ -13,6 +12,7 @@ import { useTunnels } from "../../context/tunnels";
 import { HelpButton } from "../HelpButton";
 import { Section } from "../Section";
 import Stack from "../Stack";
+import { UIAppState } from "../../types";
 
 const Footer = ({
   appState,
@@ -20,7 +20,7 @@ const Footer = ({
   showExitZenModeBtn,
   renderWelcomeScreen,
 }: {
-  appState: AppState;
+  appState: UIAppState;
   actionManager: ActionManager;
   showExitZenModeBtn: boolean;
   renderWelcomeScreen: boolean;

+ 2 - 2
src/components/footer/FooterCenter.tsx

@@ -1,11 +1,11 @@
 import clsx from "clsx";
-import { useExcalidrawAppState } from "../App";
 import { useTunnels } from "../../context/tunnels";
 import "./FooterCenter.scss";
+import { useUIAppState } from "../../context/ui-appState";
 
 const FooterCenter = ({ children }: { children?: React.ReactNode }) => {
   const { FooterCenterTunnel } = useTunnels();
-  const appState = useExcalidrawAppState();
+  const appState = useUIAppState();
   return (
     <FooterCenterTunnel.In>
       <div

+ 2 - 2
src/components/live-collaboration/LiveCollaborationTrigger.tsx

@@ -3,9 +3,9 @@ import { usersIcon } from "../icons";
 import { Button } from "../Button";
 
 import clsx from "clsx";
-import { useExcalidrawAppState } from "../App";
 
 import "./LiveCollaborationTrigger.scss";
+import { useUIAppState } from "../../context/ui-appState";
 
 const LiveCollaborationTrigger = ({
   isCollaborating,
@@ -15,7 +15,7 @@ const LiveCollaborationTrigger = ({
   isCollaborating: boolean;
   onSelect: () => void;
 } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
-  const appState = useExcalidrawAppState();
+  const appState = useUIAppState();
 
   return (
     <Button

+ 4 - 7
src/components/main-menu/DefaultItems.tsx

@@ -1,10 +1,6 @@
 import { getShortcutFromShortcutName } from "../../actions/shortcuts";
 import { useI18n } from "../../i18n";
-import {
-  useExcalidrawAppState,
-  useExcalidrawSetAppState,
-  useExcalidrawActionManager,
-} from "../App";
+import { useExcalidrawSetAppState, useExcalidrawActionManager } from "../App";
 import {
   ExportIcon,
   ExportImageIcon,
@@ -32,6 +28,7 @@ import clsx from "clsx";
 import { useSetAtom } from "jotai";
 import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
 import { jotaiScope } from "../../jotai";
+import { useUIAppState } from "../../context/ui-appState";
 
 export const LoadScene = () => {
   const { t } = useI18n();
@@ -139,7 +136,7 @@ ClearCanvas.displayName = "ClearCanvas";
 
 export const ToggleTheme = () => {
   const { t } = useI18n();
-  const appState = useExcalidrawAppState();
+  const appState = useUIAppState();
   const actionManager = useExcalidrawActionManager();
 
   if (!actionManager.isActionEnabled(actionToggleTheme)) {
@@ -172,7 +169,7 @@ ToggleTheme.displayName = "ToggleTheme";
 
 export const ChangeCanvasBackground = () => {
   const { t } = useI18n();
-  const appState = useExcalidrawAppState();
+  const appState = useUIAppState();
   const actionManager = useExcalidrawActionManager();
 
   if (appState.viewModeEnabled) {

+ 3 - 6
src/components/main-menu/MainMenu.tsx

@@ -1,9 +1,5 @@
 import React from "react";
-import {
-  useDevice,
-  useExcalidrawAppState,
-  useExcalidrawSetAppState,
-} from "../App";
+import { useDevice, useExcalidrawSetAppState } from "../App";
 import DropdownMenu from "../dropdownMenu/DropdownMenu";
 
 import * as DefaultItems from "./DefaultItems";
@@ -14,6 +10,7 @@ import { HamburgerMenuIcon } from "../icons";
 import { withInternalFallback } from "../hoc/withInternalFallback";
 import { composeEventHandlers } from "../../utils";
 import { useTunnels } from "../../context/tunnels";
+import { useUIAppState } from "../../context/ui-appState";
 
 const MainMenu = Object.assign(
   withInternalFallback(
@@ -30,7 +27,7 @@ const MainMenu = Object.assign(
     }) => {
       const { MainMenuTunnel } = useTunnels();
       const device = useDevice();
-      const appState = useExcalidrawAppState();
+      const appState = useUIAppState();
       const setAppState = useExcalidrawSetAppState();
       const onClickOutside = device.isMobile
         ? undefined

+ 3 - 6
src/components/welcome-screen/WelcomeScreen.Center.tsx

@@ -1,13 +1,10 @@
 import { actionLoadScene, actionShortcuts } from "../../actions";
 import { getShortcutFromShortcutName } from "../../actions/shortcuts";
 import { t, useI18n } from "../../i18n";
-import {
-  useDevice,
-  useExcalidrawActionManager,
-  useExcalidrawAppState,
-} from "../App";
+import { useDevice, useExcalidrawActionManager } from "../App";
 import { useTunnels } from "../../context/tunnels";
 import { ExcalLogo, HelpIcon, LoadIcon, usersIcon } from "../icons";
+import { useUIAppState } from "../../context/ui-appState";
 
 const WelcomeScreenMenuItemContent = ({
   icon,
@@ -148,7 +145,7 @@ const MenuItemHelp = () => {
 MenuItemHelp.displayName = "MenuItemHelp";
 
 const MenuItemLoadScene = () => {
-  const appState = useExcalidrawAppState();
+  const appState = useUIAppState();
   const actionManager = useExcalidrawActionManager();
 
   if (appState.viewModeEnabled) {

+ 2 - 2
src/context/ui-appState.ts

@@ -1,5 +1,5 @@
 import React from "react";
-import { AppState } from "../types";
+import { UIAppState } from "../types";
 
-export const UIAppStateContext = React.createContext<AppState>(null!);
+export const UIAppStateContext = React.createContext<UIAppState>(null!);
 export const useUIAppState = () => React.useContext(UIAppStateContext);

+ 3 - 2
src/element/Hyperlink.tsx

@@ -1,4 +1,4 @@
-import { AppState, ExcalidrawProps, Point } from "../types";
+import { AppState, ExcalidrawProps, Point, UIAppState } from "../types";
 import {
   getShortcutKey,
   sceneCoordsToViewportCoords,
@@ -297,10 +297,11 @@ export const getContextMenuLabel = (
     : "labels.link.create";
   return label;
 };
+
 export const getLinkHandleFromCoords = (
   [x1, y1, x2, y2]: Bounds,
   angle: number,
-  appState: AppState,
+  appState: UIAppState,
 ): [x: number, y: number, width: number, height: number] => {
   const size = DEFAULT_LINK_SIZE;
   const linkWidth = size / appState.zoom.value;

+ 2 - 2
src/element/showSelectedShapeActions.ts

@@ -1,9 +1,9 @@
-import { AppState } from "../types";
 import { NonDeletedExcalidrawElement } from "./types";
 import { getSelectedElements } from "../scene";
+import { UIAppState } from "../types";
 
 export const showSelectedShapeActions = (
-  appState: AppState,
+  appState: UIAppState,
   elements: readonly NonDeletedExcalidrawElement[],
 ) =>
   Boolean(

+ 3 - 2
src/excalidraw-app/CustomStats.tsx

@@ -7,8 +7,9 @@ import {
 import { DEFAULT_VERSION } from "../constants";
 import { t } from "../i18n";
 import { copyTextToSystemClipboard } from "../clipboard";
-import { AppState } from "../types";
 import { NonDeletedExcalidrawElement } from "../element/types";
+import { UIAppState } from "../types";
+
 type StorageSizes = { scene: number; total: number };
 
 const STORAGE_SIZE_TIMEOUT = 500;
@@ -23,7 +24,7 @@ const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
 type Props = {
   setToast: (message: string) => void;
   elements: readonly NonDeletedExcalidrawElement[];
-  appState: AppState;
+  appState: UIAppState;
 };
 const CustomStats = (props: Props) => {
   const [storageSizes, setStorageSizes] = useState<StorageSizes>({

+ 2 - 2
src/excalidraw-app/components/ExportToExcalidrawPlus.tsx

@@ -18,7 +18,7 @@ import { getFrame } from "../../utils";
 
 const exportToExcalidrawPlus = async (
   elements: readonly NonDeletedExcalidrawElement[],
-  appState: AppState,
+  appState: Partial<AppState>,
   files: BinaryFiles,
 ) => {
   const firebase = await loadFirebaseStorage();
@@ -75,7 +75,7 @@ const exportToExcalidrawPlus = async (
 
 export const ExportToExcalidrawPlus: React.FC<{
   elements: readonly NonDeletedExcalidrawElement[];
-  appState: AppState;
+  appState: Partial<AppState>;
   files: BinaryFiles;
   onError: (error: Error) => void;
 }> = ({ elements, appState, files, onError }) => {

+ 1 - 1
src/excalidraw-app/data/index.ts

@@ -284,7 +284,7 @@ export const loadScene = async (
 
 export const exportToBackend = async (
   elements: readonly ExcalidrawElement[],
-  appState: AppState,
+  appState: Partial<AppState>,
   files: BinaryFiles,
 ) => {
   const encryptionKey = await generateEncryptionKey("string");

+ 3 - 2
src/excalidraw-app/index.tsx

@@ -32,6 +32,7 @@ import {
   ExcalidrawImperativeAPI,
   BinaryFiles,
   ExcalidrawInitialDataState,
+  UIAppState,
 } from "../types";
 import {
   debounce,
@@ -550,7 +551,7 @@ const ExcalidrawWrapper = () => {
 
   const onExportToBackend = async (
     exportedElements: readonly NonDeletedExcalidrawElement[],
-    appState: AppState,
+    appState: Partial<AppState>,
     files: BinaryFiles,
     canvas: HTMLCanvasElement | null,
   ) => {
@@ -581,7 +582,7 @@ const ExcalidrawWrapper = () => {
 
   const renderCustomStats = (
     elements: readonly NonDeletedExcalidrawElement[],
-    appState: AppState,
+    appState: UIAppState,
   ) => {
     return (
       <CustomStats

+ 4 - 4
src/scene/selection.ts

@@ -30,7 +30,7 @@ export const getElementsWithinSelection = (
 
 export const isSomeElementSelected = (
   elements: readonly NonDeletedExcalidrawElement[],
-  appState: AppState,
+  appState: Pick<AppState, "selectedElementIds">,
 ): boolean =>
   elements.some((element) => appState.selectedElementIds[element.id]);
 
@@ -40,7 +40,7 @@ export const isSomeElementSelected = (
  */
 export const getCommonAttributeOfSelectedElements = <T>(
   elements: readonly NonDeletedExcalidrawElement[],
-  appState: AppState,
+  appState: Pick<AppState, "selectedElementIds">,
   getAttribute: (element: ExcalidrawElement) => T,
 ): T | null => {
   const attributes = Array.from(
@@ -55,7 +55,7 @@ export const getCommonAttributeOfSelectedElements = <T>(
 
 export const getSelectedElements = (
   elements: readonly NonDeletedExcalidrawElement[],
-  appState: AppState,
+  appState: Pick<AppState, "selectedElementIds">,
   includeBoundTextElement: boolean = false,
 ) =>
   elements.filter((element) => {
@@ -74,7 +74,7 @@ export const getSelectedElements = (
 
 export const getTargetElements = (
   elements: readonly NonDeletedExcalidrawElement[],
-  appState: AppState,
+  appState: Pick<AppState, "selectedElementIds" | "editingElement">,
 ) =>
   appState.editingElement
     ? [appState.editingElement]

+ 14 - 5
src/types.ts

@@ -1,3 +1,4 @@
+import React from "react";
 import {
   PointerType,
   ExcalidrawLinearElement,
@@ -32,7 +33,6 @@ import type { FileSystemHandle } from "./data/filesystem";
 import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
 import { ContextMenuItems } from "./components/ContextMenu";
 import { Merge, ForwardRef, ValueOf } from "./utility-types";
-import React from "react";
 
 export type Point = Readonly<RoughPoint>;
 
@@ -218,6 +218,15 @@ export type AppState = {
   selectedLinearElement: LinearElementEditor | null;
 };
 
+export type UIAppState = Omit<
+  AppState,
+  | "suggestedBindings"
+  | "startBoundElement"
+  | "cursorButton"
+  | "scrollX"
+  | "scrollY"
+>;
+
 export type NormalizedZoomValue = number & { _brand: "normalizedZoom" };
 
 export type Zoom = Readonly<{
@@ -314,7 +323,7 @@ export interface ExcalidrawProps {
   ) => Promise<boolean> | boolean;
   renderTopRightUI?: (
     isMobile: boolean,
-    appState: AppState,
+    appState: UIAppState,
   ) => JSX.Element | null;
   langCode?: Language["code"];
   viewModeEnabled?: boolean;
@@ -325,7 +334,7 @@ export interface ExcalidrawProps {
   name?: string;
   renderCustomStats?: (
     elements: readonly NonDeletedExcalidrawElement[],
-    appState: AppState,
+    appState: UIAppState,
   ) => JSX.Element;
   UIOptions?: Partial<UIOptions>;
   detectScroll?: boolean;
@@ -364,13 +373,13 @@ export type ExportOpts = {
   saveFileToDisk?: boolean;
   onExportToBackend?: (
     exportedElements: readonly NonDeletedExcalidrawElement[],
-    appState: AppState,
+    appState: UIAppState,
     files: BinaryFiles,
     canvas: HTMLCanvasElement | null,
   ) => void;
   renderCustomUI?: (
     exportedElements: readonly NonDeletedExcalidrawElement[],
-    appState: AppState,
+    appState: UIAppState,
     files: BinaryFiles,
     canvas: HTMLCanvasElement | null,
   ) => JSX.Element;

+ 7 - 2
src/utils.ts

@@ -372,7 +372,7 @@ export const setEraserCursor = (
 
 export const setCursorForShape = (
   canvas: HTMLCanvasElement | null,
-  appState: AppState,
+  appState: Pick<AppState, "activeTool" | "theme">,
 ) => {
   if (!canvas) {
     return;
@@ -787,7 +787,12 @@ export const isShallowEqual = <
       ? comparator(objA[key], objB[key])
       : objA[key] === objB[key];
     if (!ret && debug) {
-      console.warn(`isShallowEqual: ${key} not equal ->`, objA[key], objB[key]);
+      console.info(
+        `%cisShallowEqual: ${key} not equal ->`,
+        "color: #8B4000",
+        objA[key],
+        objB[key],
+      );
     }
     return ret;
   });