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 <5153846+dwelle@users.noreply.github.com>
pull/8952/head^2
Arnost Pleskot 2 weeks ago committed by GitHub
parent ae6bee3403
commit 8551823da9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -3,6 +3,20 @@
"rules": { "rules": {
"import/no-anonymous-default-export": "off", "import/no-anonymous-default-export": "off",
"no-restricted-globals": "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\")."
}
]
} }
} }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import tunnel from "tunnel-rat"; import tunnel from "tunnel-rat";
import { createIsolation } from "jotai-scope";
export type Tunnel = ReturnType<typeof tunnel>; export type Tunnel = ReturnType<typeof tunnel>;
@ -14,13 +15,17 @@ type TunnelsContextValue = {
DefaultSidebarTabTriggersTunnel: Tunnel; DefaultSidebarTabTriggersTunnel: Tunnel;
OverwriteConfirmDialogTunnel: Tunnel; OverwriteConfirmDialogTunnel: Tunnel;
TTDDialogTriggerTunnel: 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 TunnelsContext = React.createContext<TunnelsContextValue>(null!);
export const useTunnels = () => React.useContext(TunnelsContext); export const useTunnels = () => React.useContext(TunnelsContext);
const tunnelsJotai = createIsolation();
export const useInitializeTunnels = () => { export const useInitializeTunnels = () => {
return React.useMemo((): TunnelsContextValue => { return React.useMemo((): TunnelsContextValue => {
return { return {
@ -34,7 +39,7 @@ export const useInitializeTunnels = () => {
DefaultSidebarTabTriggersTunnel: tunnel(), DefaultSidebarTabTriggersTunnel: tunnel(),
OverwriteConfirmDialogTunnel: tunnel(), OverwriteConfirmDialogTunnel: tunnel(),
TTDDialogTriggerTunnel: tunnel(), TTDDialogTriggerTunnel: tunnel(),
jotaiScope: Symbol(), tunnelsJotai,
}; };
}, []); }, []);
}; };

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

@ -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();

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

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

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

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

@ -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;
};

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

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

Loading…
Cancel
Save