瀏覽代碼

feat: update jotai (#9015)

* feat: update jotai in excalidraw package

* feat: update jotai in excalidraw-app

* fix: exports from excalidraw/jotai

* fix: use isolated react hooks

* test: use jotai provider in <Trans /> test

* remove unused package

* refactor & make safer

---------

Co-authored-by: dwelle <[email protected]>
Arnost Pleskot 7 月之前
父節點
當前提交
8551823da9
共有 45 個文件被更改,包括 179 次插入169 次删除
  1. 15 1
      .eslintrc.json
  2. 10 7
      excalidraw-app/App.tsx
  3. 36 2
      excalidraw-app/app-jotai.ts
  4. 1 1
      excalidraw-app/app-language/LanguageList.tsx
  5. 1 1
      excalidraw-app/app-language/language-state.ts
  6. 1 2
      excalidraw-app/collab/Collab.tsx
  7. 1 1
      excalidraw-app/collab/CollabError.tsx
  8. 1 1
      excalidraw-app/package.json
  9. 3 3
      excalidraw-app/share/ShareDialog.tsx
  10. 9 10
      excalidraw-app/useHandleAppTheme.ts
  11. 1 3
      packages/excalidraw/components/ActiveConfirmDialog.tsx
  12. 5 5
      packages/excalidraw/components/App.tsx
  13. 2 6
      packages/excalidraw/components/ColorPicker/ColorInput.tsx
  14. 2 6
      packages/excalidraw/components/ColorPicker/ColorPicker.tsx
  15. 1 1
      packages/excalidraw/components/ColorPicker/CustomColorList.tsx
  16. 1 1
      packages/excalidraw/components/ColorPicker/Picker.tsx
  17. 1 1
      packages/excalidraw/components/ColorPicker/PickerColorList.tsx
  18. 1 1
      packages/excalidraw/components/ColorPicker/ShadeList.tsx
  19. 1 1
      packages/excalidraw/components/ColorPicker/colorPickerUtils.ts
  20. 2 3
      packages/excalidraw/components/CommandPalette/CommandPalette.tsx
  21. 2 3
      packages/excalidraw/components/ConfirmDialog.tsx
  22. 2 3
      packages/excalidraw/components/Dialog.tsx
  23. 1 1
      packages/excalidraw/components/EyeDropper.tsx
  24. 4 8
      packages/excalidraw/components/IconPicker.tsx
  25. 7 9
      packages/excalidraw/components/LayerUI.tsx
  26. 2 3
      packages/excalidraw/components/LibraryMenu.tsx
  27. 3 5
      packages/excalidraw/components/LibraryMenuHeaderContent.tsx
  28. 1 3
      packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm.tsx
  29. 2 3
      packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState.ts
  30. 3 7
      packages/excalidraw/components/SearchMenu.tsx
  31. 2 3
      packages/excalidraw/components/Sidebar/Sidebar.tsx
  32. 1 1
      packages/excalidraw/components/TTDDialog/TTDDialog.tsx
  33. 3 2
      packages/excalidraw/components/Trans.test.tsx
  34. 5 3
      packages/excalidraw/components/hoc/withInternalFallback.tsx
  35. 2 6
      packages/excalidraw/components/main-menu/DefaultItems.tsx
  36. 7 2
      packages/excalidraw/context/tunnels.ts
  37. 4 5
      packages/excalidraw/data/library.ts
  38. 13 0
      packages/excalidraw/editor-jotai.ts
  39. 2 3
      packages/excalidraw/hooks/useLibraryItemSvg.ts
  40. 1 1
      packages/excalidraw/hooks/useScrollPosition.ts
  41. 3 4
      packages/excalidraw/i18n.ts
  42. 3 4
      packages/excalidraw/index.tsx
  43. 0 28
      packages/excalidraw/jotai.ts
  44. 2 1
      packages/excalidraw/package.json
  45. 9 4
      yarn.lock

+ 15 - 1
.eslintrc.json

@@ -3,6 +3,20 @@
   "rules": {
     "import/no-anonymous-default-export": "off",
     "no-restricted-globals": "off",
-    "@typescript-eslint/consistent-type-imports": ["error", { "prefer": "type-imports", "disallowTypeAnnotations": false, "fixStyle": "separate-type-imports" }]
+    "@typescript-eslint/consistent-type-imports": [
+      "error",
+      {
+        "prefer": "type-imports",
+        "disallowTypeAnnotations": false,
+        "fixStyle": "separate-type-imports"
+      }
+    ],
+    "no-restricted-imports": [
+      "error",
+      {
+        "name": "jotai",
+        "message": "Do not import from \"jotai\" directly. Use our app-specific modules (\"editor-jotai\" or \"app-jotai\")."
+      }
+    ]
   }
 }

+ 10 - 7
excalidraw-app/App.tsx

@@ -90,9 +90,13 @@ import {
 import { AppMainMenu } from "./components/AppMainMenu";
 import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
 import { AppFooter } from "./components/AppFooter";
-import { Provider, useAtom, useAtomValue } from "jotai";
-import { useAtomWithInitialValue } from "../packages/excalidraw/jotai";
-import { appJotaiStore } from "./app-jotai";
+import {
+  Provider,
+  useAtom,
+  useAtomValue,
+  useAtomWithInitialValue,
+  appJotaiStore,
+} from "./app-jotai";
 
 import "./index.scss";
 import type { ResolutionType } from "../packages/excalidraw/utility-types";
@@ -117,7 +121,7 @@ import {
   share,
   youtubeIcon,
 } from "../packages/excalidraw/components/icons";
-import { appThemeAtom, useHandleAppTheme } from "./useHandleAppTheme";
+import { useHandleAppTheme } from "./useHandleAppTheme";
 import { getPreferredLanguage } from "./app-language/language-detector";
 import { useAppLangCode } from "./app-language/language-state";
 import DebugCanvas, {
@@ -328,8 +332,7 @@ const ExcalidrawWrapper = () => {
   const [errorMessage, setErrorMessage] = useState("");
   const isCollabDisabled = isRunningInIframe();
 
-  const [appTheme, setAppTheme] = useAtom(appThemeAtom);
-  const { editorTheme } = useHandleAppTheme();
+  const { editorTheme, appTheme, setAppTheme } = useHandleAppTheme();
 
   const [langCode, setLangCode] = useAppLangCode();
 
@@ -1141,7 +1144,7 @@ const ExcalidrawApp = () => {
 
   return (
     <TopErrorBoundary>
-      <Provider unstable_createStore={() => appJotaiStore}>
+      <Provider store={appJotaiStore}>
         <ExcalidrawWrapper />
       </Provider>
     </TopErrorBoundary>

+ 36 - 2
excalidraw-app/app-jotai.ts

@@ -1,3 +1,37 @@
-import { unstable_createStore } from "jotai";
+// eslint-disable-next-line no-restricted-imports
+import {
+  atom,
+  Provider,
+  useAtom,
+  useAtomValue,
+  useSetAtom,
+  createStore,
+  type PrimitiveAtom,
+} from "jotai";
+import { useLayoutEffect } from "react";
 
-export const appJotaiStore = unstable_createStore();
+export const appJotaiStore = createStore();
+
+export { atom, Provider, useAtom, useAtomValue, useSetAtom };
+
+export const useAtomWithInitialValue = <
+  T extends unknown,
+  A extends PrimitiveAtom<T>,
+>(
+  atom: A,
+  initialValue: T | (() => T),
+) => {
+  const [value, setValue] = useAtom(atom);
+
+  useLayoutEffect(() => {
+    if (typeof initialValue === "function") {
+      // @ts-ignore
+      setValue(initialValue());
+    } else {
+      setValue(initialValue);
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  return [value, setValue] as const;
+};

+ 1 - 1
excalidraw-app/app-language/LanguageList.tsx

@@ -1,6 +1,6 @@
-import { useSetAtom } from "jotai";
 import React from "react";
 import { useI18n, languages } from "../../packages/excalidraw/i18n";
+import { useSetAtom } from "../app-jotai";
 import { appLangCodeAtom } from "./language-state";
 
 export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {

+ 1 - 1
excalidraw-app/app-language/language-state.ts

@@ -1,5 +1,5 @@
-import { atom, useAtom } from "jotai";
 import { useEffect } from "react";
+import { atom, useAtom } from "../app-jotai";
 import { getPreferredLanguage, languageDetector } from "./language-detector";
 
 export const appLangCodeAtom = atom(getPreferredLanguage());

+ 1 - 2
excalidraw-app/collab/Collab.tsx

@@ -79,8 +79,7 @@ import { newElementWith } from "../../packages/excalidraw/element/mutateElement"
 import { decryptData } from "../../packages/excalidraw/data/encryption";
 import { resetBrowserStateVersions } from "../data/tabSync";
 import { LocalData } from "../data/LocalData";
-import { atom } from "jotai";
-import { appJotaiStore } from "../app-jotai";
+import { appJotaiStore, atom } from "../app-jotai";
 import type { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
 import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds";
 import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils";

+ 1 - 1
excalidraw-app/collab/CollabError.tsx

@@ -2,9 +2,9 @@ import { Tooltip } from "../../packages/excalidraw/components/Tooltip";
 import { warning } from "../../packages/excalidraw/components/icons";
 import clsx from "clsx";
 import { useEffect, useRef, useState } from "react";
+import { atom } from "../app-jotai";
 
 import "./CollabError.scss";
-import { atom } from "jotai";
 
 type ErrorIndicator = {
   message: string | null;

+ 1 - 1
excalidraw-app/package.json

@@ -32,7 +32,7 @@
     "firebase": "8.3.3",
     "i18next-browser-languagedetector": "6.1.4",
     "idb-keyval": "6.0.3",
-    "jotai": "1.13.1",
+    "jotai": "2.11.0",
     "react": "18.2.0",
     "react-dom": "18.2.0",
     "socket.io-client": "4.7.2",

+ 3 - 3
excalidraw-app/share/ShareDialog.tsx

@@ -18,11 +18,11 @@ import { TextField } from "../../packages/excalidraw/components/TextField";
 import { FilledButton } from "../../packages/excalidraw/components/FilledButton";
 import type { CollabAPI } from "../collab/Collab";
 import { activeRoomLinkAtom } from "../collab/Collab";
-import { atom, useAtom, useAtomValue } from "jotai";
-
-import "./ShareDialog.scss";
 import { useUIAppState } from "../../packages/excalidraw/context/ui-appState";
 import { useCopyStatus } from "../../packages/excalidraw/hooks/useCopiedIndicator";
+import { atom, useAtom, useAtomValue } from "../app-jotai";
+
+import "./ShareDialog.scss";
 
 type OnExportToBackend = () => void;
 type ShareDialogType = "share" | "collaborationOnly";

+ 9 - 10
excalidraw-app/useHandleAppTheme.ts

@@ -1,4 +1,3 @@
-import { atom, useAtom } from "jotai";
 import { useEffect, useLayoutEffect, useState } from "react";
 import { THEME } from "../packages/excalidraw";
 import { EVENT } from "../packages/excalidraw/constants";
@@ -6,18 +5,18 @@ import type { Theme } from "../packages/excalidraw/element/types";
 import { CODES, KEYS } from "../packages/excalidraw/keys";
 import { STORAGE_KEYS } from "./app_constants";
 
-export const appThemeAtom = atom<Theme | "system">(
-  (localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_THEME) as
-    | Theme
-    | "system"
-    | null) || THEME.LIGHT,
-);
-
 const getDarkThemeMediaQuery = (): MediaQueryList | undefined =>
   window.matchMedia?.("(prefers-color-scheme: dark)");
 
 export const useHandleAppTheme = () => {
-  const [appTheme, setAppTheme] = useAtom(appThemeAtom);
+  const [appTheme, setAppTheme] = useState<Theme | "system">(() => {
+    return (
+      (localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_THEME) as
+        | Theme
+        | "system"
+        | null) || THEME.LIGHT
+    );
+  });
   const [editorTheme, setEditorTheme] = useState<Theme>(THEME.LIGHT);
 
   useEffect(() => {
@@ -66,5 +65,5 @@ export const useHandleAppTheme = () => {
     }
   }, [appTheme]);
 
-  return { editorTheme };
+  return { editorTheme, appTheme, setAppTheme };
 };

+ 1 - 3
packages/excalidraw/components/ActiveConfirmDialog.tsx

@@ -1,7 +1,6 @@
-import { atom, useAtom } from "jotai";
 import { actionClearCanvas } from "../actions";
 import { t } from "../i18n";
-import { jotaiScope } from "../jotai";
+import { atom, useAtom } from "../editor-jotai";
 import { useExcalidrawActionManager } from "./App";
 import ConfirmDialog from "./ConfirmDialog";
 
@@ -10,7 +9,6 @@ export const activeConfirmDialogAtom = atom<"clearCanvas" | null>(null);
 export const ActiveConfirmDialog = () => {
   const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
     activeConfirmDialogAtom,
-    jotaiScope,
   );
   const actionManager = useExcalidrawActionManager();
 

+ 5 - 5
packages/excalidraw/components/App.tsx

@@ -381,7 +381,7 @@ import {
   actionWrapSelectionInFrame,
 } from "../actions/actionFrame";
 import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas";
-import { jotaiStore } from "../jotai";
+import { editorJotaiStore } from "../editor-jotai";
 import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
 import { ImageSceneDataError } from "../errors";
 import {
@@ -2077,7 +2077,7 @@ class App extends React.Component<AppProps, AppState> {
   };
 
   private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => {
-    jotaiStore.set(activeEyeDropperAtom, {
+    editorJotaiStore.set(activeEyeDropperAtom, {
       swapPreviewOnAlt: true,
       colorPickerType:
         type === "stroke" ? "elementStroke" : "elementBackground",
@@ -3325,7 +3325,7 @@ class App extends React.Component<AppProps, AppState> {
         openSidebar:
           this.state.openSidebar &&
           this.device.editor.canFitSidebar &&
-          jotaiStore.get(isSidebarDockedAtom)
+          editorJotaiStore.get(isSidebarDockedAtom)
             ? this.state.openSidebar
             : null,
         ...selectGroupsForSelectedElements(
@@ -4553,7 +4553,7 @@ class App extends React.Component<AppProps, AppState> {
         event[KEYS.CTRL_OR_CMD] &&
         (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)
       ) {
-        jotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
+        editorJotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
       }
 
       // eye dropper
@@ -6292,7 +6292,7 @@ class App extends React.Component<AppProps, AppState> {
           focus: false,
         })),
       }));
-      jotaiStore.set(searchItemInFocusAtom, null);
+      editorJotaiStore.set(searchItemInFocusAtom, null);
     }
 
     // since contextMenu options are potentially evaluated on each render,

+ 2 - 6
packages/excalidraw/components/ColorPicker/ColorInput.tsx

@@ -1,10 +1,9 @@
 import { useCallback, useEffect, useRef, useState } from "react";
 import { getColor } from "./ColorPicker";
-import { useAtom } from "jotai";
 import type { ColorPickerType } from "./colorPickerUtils";
 import { activeColorPickerSectionAtom } from "./colorPickerUtils";
 import { eyeDropperIcon } from "../icons";
-import { jotaiScope } from "../../jotai";
+import { useAtom } from "../../editor-jotai";
 import { KEYS } from "../../keys";
 import { activeEyeDropperAtom } from "../EyeDropper";
 import clsx from "clsx";
@@ -57,10 +56,7 @@ export const ColorInput = ({
     }
   }, [activeSection]);
 
-  const [eyeDropperState, setEyeDropperState] = useAtom(
-    activeEyeDropperAtom,
-    jotaiScope,
-  );
+  const [eyeDropperState, setEyeDropperState] = useAtom(activeEyeDropperAtom);
 
   useEffect(() => {
     return () => {

+ 2 - 6
packages/excalidraw/components/ColorPicker/ColorPicker.tsx

@@ -5,7 +5,6 @@ import { TopPicks } from "./TopPicks";
 import { ButtonSeparator } from "../ButtonSeparator";
 import { Picker } from "./Picker";
 import * as Popover from "@radix-ui/react-popover";
-import { useAtom } from "jotai";
 import type { ColorPickerType } from "./colorPickerUtils";
 import { activeColorPickerSectionAtom } from "./colorPickerUtils";
 import { useExcalidrawContainer } from "../App";
@@ -15,7 +14,7 @@ import PickerHeading from "./PickerHeading";
 import { t } from "../../i18n";
 import clsx from "clsx";
 import { useRef } from "react";
-import { jotaiScope } from "../../jotai";
+import { useAtom } from "../../editor-jotai";
 import { ColorInput } from "./ColorInput";
 import { activeEyeDropperAtom } from "../EyeDropper";
 import { PropertiesPopover } from "../PropertiesPopover";
@@ -76,10 +75,7 @@ const ColorPickerPopupContent = ({
   const { container } = useExcalidrawContainer();
   const [, setActiveColorPickerSection] = useAtom(activeColorPickerSectionAtom);
 
-  const [eyeDropperState, setEyeDropperState] = useAtom(
-    activeEyeDropperAtom,
-    jotaiScope,
-  );
+  const [eyeDropperState, setEyeDropperState] = useAtom(activeEyeDropperAtom);
 
   const colorInputJSX = (
     <div>

+ 1 - 1
packages/excalidraw/components/ColorPicker/CustomColorList.tsx

@@ -1,5 +1,5 @@
 import clsx from "clsx";
-import { useAtom } from "jotai";
+import { useAtom } from "../../editor-jotai";
 import { useEffect, useRef } from "react";
 import { activeColorPickerSectionAtom } from "./colorPickerUtils";
 import HotkeyLabel from "./HotkeyLabel";

+ 1 - 1
packages/excalidraw/components/ColorPicker/Picker.tsx

@@ -5,7 +5,7 @@ import type { ExcalidrawElement } from "../../element/types";
 import { ShadeList } from "./ShadeList";
 
 import PickerColorList from "./PickerColorList";
-import { useAtom } from "jotai";
+import { useAtom } from "../../editor-jotai";
 import { CustomColorList } from "./CustomColorList";
 import { colorPickerKeyNavHandler } from "./keyboardNavHandlers";
 import PickerHeading from "./PickerHeading";

+ 1 - 1
packages/excalidraw/components/ColorPicker/PickerColorList.tsx

@@ -1,5 +1,5 @@
 import clsx from "clsx";
-import { useAtom } from "jotai";
+import { useAtom } from "../../editor-jotai";
 import { useEffect, useRef } from "react";
 import {
   activeColorPickerSectionAtom,

+ 1 - 1
packages/excalidraw/components/ColorPicker/ShadeList.tsx

@@ -1,5 +1,5 @@
 import clsx from "clsx";
-import { useAtom } from "jotai";
+import { useAtom } from "../../editor-jotai";
 import { useEffect, useRef } from "react";
 import {
   activeColorPickerSectionAtom,

+ 1 - 1
packages/excalidraw/components/ColorPicker/colorPickerUtils.ts

@@ -1,7 +1,7 @@
 import type { ExcalidrawElement } from "../../element/types";
-import { atom } from "jotai";
 import type { ColorPickerColor, ColorPaletteCustom } from "../../colors";
 import { MAX_CUSTOM_COLORS_USED_IN_CANVAS } from "../../colors";
+import { atom } from "../../editor-jotai";
 
 export const getColorNameAndShadeFromColor = ({
   palette,

+ 2 - 3
packages/excalidraw/components/CommandPalette/CommandPalette.tsx

@@ -36,7 +36,7 @@ import {
   getShortcutKey,
   isWritableElement,
 } from "../../utils";
-import { atom, useAtom } from "jotai";
+import { atom, useAtom, editorJotaiStore } from "../../editor-jotai";
 import { deburr } from "../../deburr";
 import type { MarkRequired } from "../../utility-types";
 import { InlineIcon } from "../InlineIcon";
@@ -48,7 +48,6 @@ import {
   actionLink,
   actionToggleSearchMenu,
 } from "../../actions";
-import { jotaiStore } from "../../jotai";
 import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
 import type { CommandPaletteItem } from "./types";
 import * as defaultItems from "./defaultCommandPaletteItems";
@@ -349,7 +348,7 @@ function CommandPaletteInner({
           keywords: ["delete", "destroy"],
           viewMode: false,
           perform: () => {
-            jotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
+            editorJotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
           },
         },
         {

+ 2 - 3
packages/excalidraw/components/ConfirmDialog.tsx

@@ -5,10 +5,9 @@ import { Dialog } from "./Dialog";
 
 import "./ConfirmDialog.scss";
 import DialogActionButton from "./DialogActionButton";
-import { useSetAtom } from "jotai";
 import { isLibraryMenuOpenAtom } from "./LibraryMenu";
 import { useExcalidrawContainer, useExcalidrawSetAppState } from "./App";
-import { jotaiScope } from "../jotai";
+import { useSetAtom } from "../editor-jotai";
 
 interface Props extends Omit<DialogProps, "onCloseRequest"> {
   onConfirm: () => void;
@@ -27,7 +26,7 @@ const ConfirmDialog = (props: Props) => {
     ...rest
   } = props;
   const setAppState = useExcalidrawSetAppState();
-  const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope);
+  const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
   const { container } = useExcalidrawContainer();
 
   return (

+ 2 - 3
packages/excalidraw/components/Dialog.tsx

@@ -11,9 +11,8 @@ import "./Dialog.scss";
 import { Island } from "./Island";
 import { Modal } from "./Modal";
 import { queryFocusableElements } from "../utils";
-import { useSetAtom } from "jotai";
 import { isLibraryMenuOpenAtom } from "./LibraryMenu";
-import { jotaiScope } from "../jotai";
+import { useSetAtom } from "../editor-jotai";
 import { t } from "../i18n";
 import { CloseIcon } from "./icons";
 
@@ -92,7 +91,7 @@ export const Dialog = (props: DialogProps) => {
   }, [islandNode, props.autofocus]);
 
   const setAppState = useExcalidrawSetAppState();
-  const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope);
+  const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
 
   const onClose = () => {
     setAppState({ openMenu: null });

+ 1 - 1
packages/excalidraw/components/EyeDropper.tsx

@@ -1,4 +1,3 @@
-import { atom } from "jotai";
 import { useEffect, useRef } from "react";
 import { createPortal } from "react-dom";
 import { rgbToHex } from "../colors";
@@ -14,6 +13,7 @@ import { useStable } from "../hooks/useStable";
 import "./EyeDropper.scss";
 import type { ColorPickerType } from "./ColorPicker/colorPickerUtils";
 import type { ExcalidrawElement } from "../element/types";
+import { atom } from "../editor-jotai";
 
 export type EyeDropperProperties = {
   keepOpenOnAlt: boolean;

+ 4 - 8
packages/excalidraw/components/IconPicker.tsx

@@ -1,15 +1,14 @@
 import React, { useEffect } from "react";
 import * as Popover from "@radix-ui/react-popover";
-
-import "./IconPicker.scss";
 import { isArrowKey, KEYS } from "../keys";
 import { getLanguage, t } from "../i18n";
 import clsx from "clsx";
 import Collapsible from "./Stats/Collapsible";
-import { atom, useAtom } from "jotai";
-import { jotaiScope } from "../jotai";
+import { atom, useAtom } from "../editor-jotai";
 import { useDevice } from "..";
 
+import "./IconPicker.scss";
+
 const moreOptionsAtom = atom(false);
 
 type Option<T> = {
@@ -94,10 +93,7 @@ function Picker<T>({
     event.stopPropagation();
   };
 
-  const [showMoreOptions, setShowMoreOptions] = useAtom(
-    moreOptionsAtom,
-    jotaiScope,
-  );
+  const [showMoreOptions, setShowMoreOptions] = useAtom(moreOptionsAtom);
 
   const alwaysVisibleOptions = React.useMemo(
     () => options.slice(0, numberOfOptionsToAlwaysShow),

+ 7 - 9
packages/excalidraw/components/LayerUI.tsx

@@ -41,8 +41,7 @@ import { trackEvent } from "../analytics";
 import { useDevice } from "./App";
 import Footer from "./footer/Footer";
 import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
-import { jotaiScope } from "../jotai";
-import { Provider, useAtom, useAtomValue } from "jotai";
+import { useAtom, useAtomValue } from "../editor-jotai";
 import MainMenu from "./main-menu/MainMenu";
 import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
 import { OverwriteConfirmDialog } from "./OverwriteConfirm/OverwriteConfirm";
@@ -148,10 +147,9 @@ const LayerUI = ({
   const device = useDevice();
   const tunnels = useInitializeTunnels();
 
-  const [eyeDropperState, setEyeDropperState] = useAtom(
-    activeEyeDropperAtom,
-    jotaiScope,
-  );
+  const TunnelsJotaiProvider = tunnels.tunnelsJotai.Provider;
+
+  const [eyeDropperState, setEyeDropperState] = useAtom(activeEyeDropperAtom);
 
   const renderJSONExportDialog = () => {
     if (!UIOptions.canvasActions.export) {
@@ -382,7 +380,7 @@ const LayerUI = ({
     );
   };
 
-  const isSidebarDocked = useAtomValue(isSidebarDockedAtom, jotaiScope);
+  const isSidebarDocked = useAtomValue(isSidebarDockedAtom);
 
   const layerUIJSX = (
     <>
@@ -566,11 +564,11 @@ const LayerUI = ({
 
   return (
     <UIAppStateContext.Provider value={appState}>
-      <Provider scope={tunnels.jotaiScope}>
+      <TunnelsJotaiProvider>
         <TunnelsContext.Provider value={tunnels}>
           {layerUIJSX}
         </TunnelsContext.Provider>
-      </Provider>
+      </TunnelsJotaiProvider>
     </UIAppStateContext.Provider>
   );
 };

+ 2 - 3
packages/excalidraw/components/LibraryMenu.tsx

@@ -14,8 +14,7 @@ import type {
 } from "../types";
 import LibraryMenuItems from "./LibraryMenuItems";
 import { trackEvent } from "../analytics";
-import { atom, useAtom } from "jotai";
-import { jotaiScope } from "../jotai";
+import { atom, useAtom } from "../editor-jotai";
 import Spinner from "./Spinner";
 import {
   useApp,
@@ -61,7 +60,7 @@ export const LibraryMenuContent = ({
   selectedItems: LibraryItem["id"][];
   onSelectItems: (id: LibraryItem["id"][]) => void;
 }) => {
-  const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
+  const [libraryItemsData] = useAtom(libraryItemsAtom);
 
   const _onAddToLibrary = useCallback(
     (elements: LibraryItem["elements"]) => {

+ 3 - 5
packages/excalidraw/components/LibraryMenuHeaderContent.tsx

@@ -1,7 +1,7 @@
 import { useCallback, useState } from "react";
 import { t } from "../i18n";
 import Trans from "./Trans";
-import { jotaiScope } from "../jotai";
+import { useAtom } from "../editor-jotai";
 import type { LibraryItem, LibraryItems, UIAppState } from "../types";
 import { useApp, useExcalidrawSetAppState } from "./App";
 import { saveLibraryAsJSON } from "../data/json";
@@ -17,7 +17,6 @@ import {
 import { ToolButton } from "./ToolButton";
 import { fileOpen } from "../data/filesystem";
 import { muteFSAbortError } from "../utils";
-import { useAtom } from "jotai";
 import ConfirmDialog from "./ConfirmDialog";
 import PublishLibrary from "./PublishLibrary";
 import { Dialog } from "./Dialog";
@@ -51,10 +50,9 @@ export const LibraryDropdownMenuButton: React.FC<{
   appState,
   className,
 }) => {
-  const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
+  const [libraryItemsData] = useAtom(libraryItemsAtom);
   const [isLibraryMenuOpen, setIsLibraryMenuOpen] = useAtom(
     isLibraryMenuOpenAtom,
-    jotaiScope,
   );
 
   const renderRemoveLibAlert = () => {
@@ -286,7 +284,7 @@ export const LibraryDropdownMenu = ({
   const appState = useUIAppState();
   const setAppState = useExcalidrawSetAppState();
 
-  const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
+  const [libraryItemsData] = useAtom(libraryItemsAtom);
 
   const removeFromLibrary = async (libraryItems: LibraryItems) => {
     const nextItems = libraryItems.filter(

+ 1 - 3
packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm.tsx

@@ -1,8 +1,7 @@
 import React from "react";
-import { useAtom } from "jotai";
 
 import { useTunnels } from "../../context/tunnels";
-import { jotaiScope } from "../../jotai";
+import { useAtom } from "../../editor-jotai";
 import { Dialog } from "../Dialog";
 import { withInternalFallback } from "../hoc/withInternalFallback";
 import { overwriteConfirmStateAtom } from "./OverwriteConfirmState";
@@ -23,7 +22,6 @@ const OverwriteConfirmDialog = Object.assign(
       const { OverwriteConfirmDialogTunnel } = useTunnels();
       const [overwriteConfirmState, setState] = useAtom(
         overwriteConfirmStateAtom,
-        jotaiScope,
       );
 
       if (!overwriteConfirmState.active) {

+ 2 - 3
packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState.ts

@@ -1,5 +1,4 @@
-import { atom } from "jotai";
-import { jotaiStore } from "../../jotai";
+import { atom, editorJotaiStore } from "../../editor-jotai";
 import type React from "react";
 
 export type OverwriteConfirmState =
@@ -32,7 +31,7 @@ export async function openConfirmModal({
   color: "danger" | "warning";
 }) {
   return new Promise<boolean>((resolve) => {
-    jotaiStore.set(overwriteConfirmStateAtom, {
+    editorJotaiStore.set(overwriteConfirmStateAtom, {
       active: true,
       onConfirm: () => resolve(true),
       onClose: () => resolve(false),

+ 3 - 7
packages/excalidraw/components/SearchMenu.tsx

@@ -11,8 +11,7 @@ import { measureText } from "../element/textElement";
 import { addEventListener, getFontString } from "../utils";
 import { KEYS } from "../keys";
 import clsx from "clsx";
-import { atom, useAtom } from "jotai";
-import { jotaiScope } from "../jotai";
+import { atom, useAtom } from "../editor-jotai";
 import { t } from "../i18n";
 import { isElementCompletelyInViewport } from "../element/sizeHelpers";
 import { randomInteger } from "../random";
@@ -58,7 +57,7 @@ export const SearchMenu = () => {
 
   const searchInputRef = useRef<HTMLInputElement>(null);
 
-  const [inputValue, setInputValue] = useAtom(searchQueryAtom, jotaiScope);
+  const [inputValue, setInputValue] = useAtom(searchQueryAtom);
   const searchQuery = inputValue.trim() as SearchQuery;
 
   const [isSearching, setIsSearching] = useState(false);
@@ -70,10 +69,7 @@ export const SearchMenu = () => {
   const searchedQueryRef = useRef<SearchQuery | null>(null);
   const lastSceneNonceRef = useRef<number | undefined>(undefined);
 
-  const [focusIndex, setFocusIndex] = useAtom(
-    searchItemInFocusAtom,
-    jotaiScope,
-  );
+  const [focusIndex, setFocusIndex] = useAtom(searchItemInFocusAtom);
   const elementsMap = app.scene.getNonDeletedElementsMap();
 
   useEffect(() => {

+ 2 - 3
packages/excalidraw/components/Sidebar/Sidebar.tsx

@@ -8,8 +8,7 @@ import React, {
   useCallback,
 } from "react";
 import { Island } from "../Island";
-import { atom, useSetAtom } from "jotai";
-import { jotaiScope } from "../../jotai";
+import { atom, useSetAtom } from "../../editor-jotai";
 import type { SidebarProps, SidebarPropsContextValue } from "./common";
 import { SidebarPropsContext } from "./common";
 import { SidebarHeader } from "./SidebarHeader";
@@ -58,7 +57,7 @@ export const SidebarInner = forwardRef(
 
     const setAppState = useExcalidrawSetAppState();
 
-    const setIsSidebarDockedAtom = useSetAtom(isSidebarDockedAtom, jotaiScope);
+    const setIsSidebarDockedAtom = useSetAtom(isSidebarDockedAtom);
 
     useLayoutEffect(() => {
       setIsSidebarDockedAtom(!!docked);

+ 1 - 1
packages/excalidraw/components/TTDDialog/TTDDialog.tsx

@@ -25,7 +25,7 @@ import type { BinaryFiles } from "../../types";
 import { ArrowRightIcon } from "../icons";
 
 import "./TTDDialog.scss";
-import { atom, useAtom } from "jotai";
+import { atom, useAtom } from "../../editor-jotai";
 import { trackEvent } from "../../analytics";
 import { InlineIcon } from "../InlineIcon";
 import { TTDDialogSubmitShortcut } from "./TTDDialogSubmitShortcut";

+ 3 - 2
packages/excalidraw/components/Trans.test.tsx

@@ -4,6 +4,7 @@ import fallbackLangData from "../locales/en.json";
 
 import Trans from "./Trans";
 import type { TranslationKeys } from "../i18n";
+import { EditorJotaiProvider } from "../editor-jotai";
 
 describe("Test <Trans/>", () => {
   it("should translate the the strings correctly", () => {
@@ -17,7 +18,7 @@ describe("Test <Trans/>", () => {
     };
 
     const { getByTestId } = render(
-      <>
+      <EditorJotaiProvider>
         <div data-testid="test1">
           <Trans
             i18nKey={"transTest.key1" as unknown as TranslationKeys}
@@ -51,7 +52,7 @@ describe("Test <Trans/>", () => {
             connect-link={(el) => <a href="https://example.com">{el}</a>}
           />
         </div>
-      </>,
+      </EditorJotaiProvider>,
     );
 
     expect(getByTestId("test1").innerHTML).toEqual("Hello world");

+ 5 - 3
packages/excalidraw/components/hoc/withInternalFallback.tsx

@@ -1,6 +1,6 @@
-import { atom, useAtom } from "jotai";
 import React, { useLayoutEffect, useRef } from "react";
 import { useTunnels } from "../../context/tunnels";
+import { atom } from "../../editor-jotai";
 
 export const withInternalFallback = <P,>(
   componentName: string,
@@ -13,9 +13,11 @@ export const withInternalFallback = <P,>(
       __fallback?: boolean;
     }
   > = (props) => {
-    const { jotaiScope } = useTunnels();
+    const {
+      tunnelsJotai: { useAtom },
+    } = useTunnels();
     // for rerenders
-    const [, setCounter] = useAtom(renderAtom, jotaiScope);
+    const [, setCounter] = useAtom(renderAtom);
     // for initial & subsequent renders. Tracked as component state
     // due to excalidraw multi-instance scanerios.
     const metaRef = useRef({

+ 2 - 6
packages/excalidraw/components/main-menu/DefaultItems.tsx

@@ -32,9 +32,8 @@ import {
   actionToggleTheme,
 } from "../../actions";
 import clsx from "clsx";
-import { useSetAtom } from "jotai";
 import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
-import { jotaiScope } from "../../jotai";
+import { useSetAtom } from "../../editor-jotai";
 import { useUIAppState } from "../../context/ui-appState";
 import { openConfirmModal } from "../OverwriteConfirm/OverwriteConfirmState";
 import Trans from "../Trans";
@@ -189,10 +188,7 @@ Help.displayName = "Help";
 export const ClearCanvas = () => {
   const { t } = useI18n();
 
-  const setActiveConfirmDialog = useSetAtom(
-    activeConfirmDialogAtom,
-    jotaiScope,
-  );
+  const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom);
   const actionManager = useExcalidrawActionManager();
 
   if (!actionManager.isActionEnabled(actionClearCanvas)) {

+ 7 - 2
packages/excalidraw/context/tunnels.ts

@@ -1,5 +1,6 @@
 import React from "react";
 import tunnel from "tunnel-rat";
+import { createIsolation } from "jotai-scope";
 
 export type Tunnel = ReturnType<typeof tunnel>;
 
@@ -14,13 +15,17 @@ type TunnelsContextValue = {
   DefaultSidebarTabTriggersTunnel: Tunnel;
   OverwriteConfirmDialogTunnel: Tunnel;
   TTDDialogTriggerTunnel: Tunnel;
-  jotaiScope: symbol;
+  // this can be removed once we create jotai stores per each editor
+  // instance
+  tunnelsJotai: ReturnType<typeof createIsolation>;
 };
 
 export const TunnelsContext = React.createContext<TunnelsContextValue>(null!);
 
 export const useTunnels = () => React.useContext(TunnelsContext);
 
+const tunnelsJotai = createIsolation();
+
 export const useInitializeTunnels = () => {
   return React.useMemo((): TunnelsContextValue => {
     return {
@@ -34,7 +39,7 @@ export const useInitializeTunnels = () => {
       DefaultSidebarTabTriggersTunnel: tunnel(),
       OverwriteConfirmDialogTunnel: tunnel(),
       TTDDialogTriggerTunnel: tunnel(),
-      jotaiScope: Symbol(),
+      tunnelsJotai,
     };
   }, []);
 };

+ 4 - 5
packages/excalidraw/data/library.ts

@@ -8,8 +8,7 @@ import type {
 } from "../types";
 import { restoreLibraryItems } from "./restore";
 import type App from "../components/App";
-import { atom } from "jotai";
-import { jotaiStore } from "../jotai";
+import { atom, editorJotaiStore } from "../editor-jotai";
 import type { ExcalidrawElement } from "../element/types";
 import { getCommonBoundingBox } from "../element/bounds";
 import { AbortError } from "../errors";
@@ -191,13 +190,13 @@ class Library {
 
   private notifyListeners = () => {
     if (this.updateQueue.length > 0) {
-      jotaiStore.set(libraryItemsAtom, (s) => ({
+      editorJotaiStore.set(libraryItemsAtom, (s) => ({
         status: "loading",
         libraryItems: this.currLibraryItems,
         isInitialized: s.isInitialized,
       }));
     } else {
-      jotaiStore.set(libraryItemsAtom, {
+      editorJotaiStore.set(libraryItemsAtom, {
         status: "loaded",
         libraryItems: this.currLibraryItems,
         isInitialized: true,
@@ -225,7 +224,7 @@ class Library {
   destroy = () => {
     this.updateQueue = [];
     this.currLibraryItems = [];
-    jotaiStore.set(libraryItemSvgsCache, new Map());
+    editorJotaiStore.set(libraryItemSvgsCache, new Map());
     // TODO uncomment after/if we make jotai store scoped to each excal instance
     // jotaiStore.set(libraryItemsAtom, {
     //   status: "loading",

+ 13 - 0
packages/excalidraw/editor-jotai.ts

@@ -0,0 +1,13 @@
+// eslint-disable-next-line no-restricted-imports
+import { atom, createStore, type PrimitiveAtom } from "jotai";
+import { createIsolation } from "jotai-scope";
+
+const jotai = createIsolation();
+
+export { atom, PrimitiveAtom };
+export const { useAtom, useSetAtom, useAtomValue, useStore } = jotai;
+export const EditorJotaiProvider: ReturnType<
+  typeof createIsolation
+>["Provider"] = jotai.Provider;
+
+export const editorJotaiStore: ReturnType<typeof createStore> = createStore();

+ 2 - 3
packages/excalidraw/hooks/useLibraryItemSvg.ts

@@ -1,7 +1,6 @@
-import { atom, useAtom } from "jotai";
 import { useEffect, useState } from "react";
 import { COLOR_PALETTE } from "../colors";
-import { jotaiScope } from "../jotai";
+import { atom, useAtom } from "../editor-jotai";
 import { exportToSvg } from "../../utils/export";
 import type { LibraryItem } from "../types";
 
@@ -64,7 +63,7 @@ export const useLibraryItemSvg = (
 };
 
 export const useLibraryCache = () => {
-  const [svgCache] = useAtom(libraryItemSvgsCache, jotaiScope);
+  const [svgCache] = useAtom(libraryItemSvgsCache);
 
   const clearLibraryCache = () => svgCache.clear();
 

+ 1 - 1
packages/excalidraw/hooks/useScrollPosition.ts

@@ -1,5 +1,5 @@
 import { useEffect } from "react";
-import { atom, useAtom } from "jotai";
+import { atom, useAtom } from "../editor-jotai";
 import throttle from "lodash.throttle";
 
 const scrollPositionAtom = atom<number>(0);

+ 3 - 4
packages/excalidraw/i18n.ts

@@ -1,7 +1,6 @@
 import fallbackLangData from "./locales/en.json";
 import percentages from "./locales/percentages.json";
-import { jotaiScope, jotaiStore } from "./jotai";
-import { atom, useAtomValue } from "jotai";
+import { useAtomValue, editorJotaiStore, atom } from "./editor-jotai";
 import type { NestedKeyOf } from "./utility-types";
 
 const COMPLETION_THRESHOLD = 85;
@@ -103,7 +102,7 @@ export const setLanguage = async (lang: Language) => {
     }
   }
 
-  jotaiStore.set(editorLangCodeAtom, lang.code);
+  editorJotaiStore.set(editorLangCodeAtom, lang.code);
 };
 
 export const getLanguage = () => currentLang;
@@ -165,6 +164,6 @@ const editorLangCodeAtom = atom(defaultLang.code);
 // - component is rendered internally by <Excalidraw>, but the component
 //   is memoized w/o being updated on `langCode`, `AppState`, or `UIAppState`
 export const useI18n = () => {
-  const langCode = useAtomValue(editorLangCodeAtom, jotaiScope);
+  const langCode = useAtomValue(editorLangCodeAtom);
   return { t, langCode };
 };

+ 3 - 4
packages/excalidraw/index.tsx

@@ -11,8 +11,7 @@ import "./fonts/fonts.css";
 import type { AppProps, ExcalidrawProps } from "./types";
 import { defaultLang } from "./i18n";
 import { DEFAULT_UI_OPTIONS } from "./constants";
-import { Provider } from "jotai";
-import { jotaiScope, jotaiStore } from "./jotai";
+import { EditorJotaiProvider, editorJotaiStore } from "./editor-jotai";
 import Footer from "./components/footer/FooterCenter";
 import MainMenu from "./components/main-menu/MainMenu";
 import WelcomeScreen from "./components/welcome-screen/WelcomeScreen";
@@ -108,7 +107,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
   }, []);
 
   return (
-    <Provider unstable_createStore={() => jotaiStore} scope={jotaiScope}>
+    <EditorJotaiProvider store={editorJotaiStore}>
       <InitializeApp langCode={langCode} theme={theme}>
         <App
           onChange={onChange}
@@ -145,7 +144,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
           {children}
         </App>
       </InitializeApp>
-    </Provider>
+    </EditorJotaiProvider>
   );
 };
 

+ 0 - 28
packages/excalidraw/jotai.ts

@@ -1,28 +0,0 @@
-import type { PrimitiveAtom } from "jotai";
-import { unstable_createStore, useAtom } from "jotai";
-import { useLayoutEffect } from "react";
-
-export const jotaiScope = Symbol();
-export const jotaiStore = unstable_createStore();
-
-export const useAtomWithInitialValue = <
-  T extends unknown,
-  A extends PrimitiveAtom<T>,
->(
-  atom: A,
-  initialValue: T | (() => T),
-) => {
-  const [value, setValue] = useAtom(atom);
-
-  useLayoutEffect(() => {
-    if (typeof initialValue === "function") {
-      // @ts-ignore
-      setValue(initialValue());
-    } else {
-      setValue(initialValue);
-    }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, []);
-
-  return [value, setValue] as const;
-};

+ 2 - 1
packages/excalidraw/package.json

@@ -70,7 +70,8 @@
     "fractional-indexing": "3.2.0",
     "fuzzy": "0.1.3",
     "image-blob-reduce": "3.0.1",
-    "jotai": "1.13.1",
+    "jotai": "2.11.0",
+    "jotai-scope": "0.7.2",
     "lodash.throttle": "4.1.1",
     "nanoid": "3.3.3",
     "open-color": "1.9.1",

+ 9 - 4
yarn.lock

@@ -7339,10 +7339,15 @@ jest-worker@^27.4.5:
     merge-stream "^2.0.0"
     supports-color "^8.0.0"
 
[email protected]:
-  version "1.13.1"
-  resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.13.1.tgz#20cc46454cbb39096b12fddfa635b873b3668236"
-  integrity sha512-RUmH1S4vLsG3V6fbGlKzGJnLrDcC/HNb5gH2AeA9DzuJknoVxSGvvg8OBB7lke+gDc4oXmdVsaKn/xDUhWZ0vw==
[email protected]:
+  version "0.7.2"
+  resolved "https://registry.yarnpkg.com/jotai-scope/-/jotai-scope-0.7.2.tgz#3e9ec5b743bd9f36b08b32cf5151786049bfcce7"
+  integrity sha512-Gwed97f3dDObrO43++2lRcgOqw4O2sdr4JCjP/7eHK1oPACDJ7xKHGScpJX9XaflU+KBHXF+VhwECnzcaQiShg==
+
[email protected]:
+  version "2.11.0"
+  resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.11.0.tgz#923f8351e0b2d721036af892c0ae25625049d120"
+  integrity sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ==
 
 "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
   version "4.0.0"