From df75fa71a7b91c84d5cfd29054b22ebef7742a31 Mon Sep 17 00:00:00 2001
From: "Sakamoto, Kazunori"
Date: Sun, 19 Apr 2026 10:22:17 +0900
Subject: [PATCH] refactor: simplify monaco initialization
Co-authored-by: WillBooster (Codex CLI)
---
README.md | 2 +-
e2e/next-app/app/editorProbe.tsx | 17 +++++--
src/DiffEditor/DiffEditor.tsx | 73 +++++++++++++++++++++++-------
src/Editor/Editor.tsx | 74 ++++++++++++++++++++++++-------
src/MonacoContainer/types.ts | 2 +-
src/hooks/useMonaco/index.ts | 52 +++++++++++-----------
src/hooks/useMonaco/initMonaco.ts | 12 +++++
7 files changed, 168 insertions(+), 64 deletions(-)
create mode 100644 src/hooks/useMonaco/initMonaco.ts
diff --git a/README.md b/README.md
index 8b92c55..ae63faa 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@ import Editor, { DiffEditor, loader, useMonaco } from '@willbooster/monaco-react
- `Editor`: Monaco standalone code editor component.
- `DiffEditor`: Monaco standalone diff editor component.
-- `useMonaco`: React hook returning the initialized Monaco instance.
+- `useMonaco`: React hook returning the initialized Monaco instance after hydration. Wrap components that call it in `Suspense`.
- `loader`: the `@willbooster/monaco-loader` instance used by the components.
### Editor
diff --git a/e2e/next-app/app/editorProbe.tsx b/e2e/next-app/app/editorProbe.tsx
index 3d9268d..f465055 100644
--- a/e2e/next-app/app/editorProbe.tsx
+++ b/e2e/next-app/app/editorProbe.tsx
@@ -1,17 +1,18 @@
'use client';
-import { useState } from 'react';
+import { Suspense, useState } from 'react';
import MonacoEditor, { DiffEditor, useMonaco } from '@willbooster/monaco-react';
export default function EditorProbe() {
const [editorStatus, setEditorStatus] = useState('editor-pending');
const [diffStatus, setDiffStatus] = useState('diff-pending');
- const loadedMonaco = useMonaco();
return (
<>
- {loadedMonaco?.editor ? 'hook-ok' : 'hook-pending'}
+ hook-pending
}>
+
+
{editorStatus}
);
}
+
+function HookStatus() {
+ const loadedMonaco = useMonaco();
+
+ if (!loadedMonaco) {
+ return hook-pending
;
+ }
+
+ return {loadedMonaco.editor ? 'hook-ok' : 'hook-mismatch'}
;
+}
diff --git a/src/DiffEditor/DiffEditor.tsx b/src/DiffEditor/DiffEditor.tsx
index 1ef471c..bbb2733 100644
--- a/src/DiffEditor/DiffEditor.tsx
+++ b/src/DiffEditor/DiffEditor.tsx
@@ -1,17 +1,60 @@
'use client';
-import { useState, useRef, useCallback, useEffect } from 'react';
+import { Suspense, useState, useRef, useCallback, useEffect } from 'react';
import type { ReactElement } from 'react';
-import loader from '@willbooster/monaco-loader';
import MonacoContainer from '../MonacoContainer';
import useMount from '../hooks/useMount';
import useUpdate from '../hooks/useUpdate';
+import useMonaco from '../hooks/useMonaco';
import { noop, getOrCreateModel } from '../utils';
import type { DiffEditorProps, MonacoDiffEditor } from './types';
import type { Monaco } from '..';
-function DiffEditor({
+function DiffEditor(props: DiffEditorProps): ReactElement {
+ const { width = '100%', height = '100%', loading = 'Loading...', className, wrapperProps = {} } = props;
+
+ return (
+
+ }
+ >
+
+
+ );
+}
+
+function DiffEditorContent(props: DiffEditorProps): ReactElement {
+ const monaco = useMonaco();
+
+ if (!monaco) {
+ const { width = '100%', height = '100%', loading = 'Loading...', className, wrapperProps = {} } = props;
+
+ return (
+
+ );
+ }
+
+ return ;
+}
+
+function MountedDiffEditor({
+ monaco,
original,
modified,
language,
@@ -30,24 +73,23 @@ function DiffEditor({
wrapperProps = {},
beforeMount = noop,
onMount = noop,
-}: DiffEditorProps): ReactElement {
+}: DiffEditorProps & { monaco: Monaco }): ReactElement {
const [isEditorReady, setIsEditorReady] = useState(false);
- const [isMonacoMounting, setIsMonacoMounting] = useState(true);
const editorRef = useRef(null);
- const monacoRef = useRef(null);
+ const monacoRef = useRef(monaco);
const containerRef = useRef(null);
const onMountRef = useRef(onMount);
const beforeMountRef = useRef(beforeMount);
const preventCreation = useRef(false);
- useMount(() => {
- const cancelable = loader.init();
+ monacoRef.current = monaco;
- cancelable
- .then((monaco) => (monacoRef.current = monaco as Monaco) && setIsMonacoMounting(false))
- .catch((error) => error?.type !== 'cancelation' && console.error('Monaco initialization: error:', error));
-
- return () => (editorRef.current ? disposeEditor() : cancelable.cancel());
+ useMount(() => {
+ return () => {
+ if (editorRef.current) {
+ disposeEditor();
+ }
+ };
});
useUpdate(
@@ -149,7 +191,6 @@ function DiffEditor({
);
const setModels = useCallback(() => {
- if (!monacoRef.current) return;
beforeMountRef.current(monacoRef.current);
const originalModel = getOrCreateModel(
monacoRef.current,
@@ -194,10 +235,10 @@ function DiffEditor({
}, [isEditorReady]);
useEffect(() => {
- if (!isMonacoMounting && !isEditorReady) {
+ if (!isEditorReady) {
createEditor();
}
- }, [isMonacoMounting, isEditorReady, createEditor]);
+ }, [isEditorReady, createEditor]);
function disposeEditor(): void {
const editor = editorRef.current;
diff --git a/src/Editor/Editor.tsx b/src/Editor/Editor.tsx
index a0bb20d..f30d8f2 100644
--- a/src/Editor/Editor.tsx
+++ b/src/Editor/Editor.tsx
@@ -1,11 +1,11 @@
'use client';
-import { useState, useEffect, useRef, useCallback } from 'react';
+import { Suspense, useState, useEffect, useRef, useCallback } from 'react';
import type { ReactElement } from 'react';
-import loader from '@willbooster/monaco-loader';
import useMount from '../hooks/useMount';
import useUpdate from '../hooks/useUpdate';
import usePrevious from '../hooks/usePrevious';
+import useMonaco from '../hooks/useMonaco';
import type { IDisposable, Uri, editor } from 'monaco-editor/esm/vs/editor/editor.api.js';
import { noop, getOrCreateModel } from '../utils';
import type { EditorProps } from './types';
@@ -14,7 +14,50 @@ import MonacoContainer from '../MonacoContainer';
const viewStates = new Map();
-function Editor({
+function Editor(props: EditorProps): ReactElement {
+ const { width = '100%', height = '100%', loading = 'Loading...', className, wrapperProps = {} } = props;
+
+ return (
+
+ }
+ >
+
+
+ );
+}
+
+function EditorContent(props: EditorProps): ReactElement {
+ const monaco = useMonaco();
+
+ if (!monaco) {
+ const { width = '100%', height = '100%', loading = 'Loading...', className, wrapperProps = {} } = props;
+
+ return (
+
+ );
+ }
+
+ return ;
+}
+
+function MountedEditor({
+ monaco,
defaultValue,
defaultLanguage,
defaultPath,
@@ -39,10 +82,9 @@ function Editor({
onMount = noop,
onChange,
onValidate = noop,
-}: EditorProps): ReactElement {
+}: EditorProps & { monaco: Monaco }): ReactElement {
const [isEditorReady, setIsEditorReady] = useState(false);
- const [isMonacoMounting, setIsMonacoMounting] = useState(true);
- const monacoRef = useRef(null);
+ const monacoRef = useRef(monaco);
const editorRef = useRef(undefined);
const containerRef = useRef(null);
const onMountRef = useRef(onMount);
@@ -53,14 +95,14 @@ function Editor({
const preventCreation = useRef(false);
const preventTriggerChangeEvent = useRef(false);
- useMount(() => {
- const cancelable = loader.init();
-
- cancelable
- .then((monaco) => (monacoRef.current = monaco as Monaco) && setIsMonacoMounting(false))
- .catch((error) => error?.type !== 'cancelation' && console.error('Monaco initialization: error:', error));
+ monacoRef.current = monaco;
- return () => (editorRef.current ? disposeEditor() : cancelable.cancel());
+ useMount(() => {
+ return () => {
+ if (editorRef.current) {
+ disposeEditor();
+ }
+ };
});
useUpdate(
@@ -144,7 +186,7 @@ function Editor({
);
const createEditor = useCallback(() => {
- if (!containerRef.current || !monacoRef.current) return;
+ if (!containerRef.current) return;
if (!preventCreation.current) {
beforeMountRef.current(monacoRef.current);
const autoCreatedModelPath = path || defaultPath;
@@ -201,10 +243,10 @@ function Editor({
}, [isEditorReady]);
useEffect(() => {
- if (!isMonacoMounting && !isEditorReady) {
+ if (!isEditorReady) {
createEditor();
}
- }, [isMonacoMounting, isEditorReady, createEditor]);
+ }, [isEditorReady, createEditor]);
// subscription
// to avoid unnecessary updates (attach - dispose listener) in subscription
diff --git a/src/MonacoContainer/types.ts b/src/MonacoContainer/types.ts
index 863e3e9..1ac2cb6 100644
--- a/src/MonacoContainer/types.ts
+++ b/src/MonacoContainer/types.ts
@@ -5,7 +5,7 @@ export interface ContainerProps {
height: number | string;
isEditorReady: boolean;
loading: ReactNode | string;
- _ref: RefObject;
+ _ref?: RefObject;
className?: string;
wrapperProps?: object;
}
diff --git a/src/hooks/useMonaco/index.ts b/src/hooks/useMonaco/index.ts
index 977b392..81bb95f 100644
--- a/src/hooks/useMonaco/index.ts
+++ b/src/hooks/useMonaco/index.ts
@@ -1,34 +1,32 @@
-import { useState } from 'react';
-import loader from '@willbooster/monaco-loader';
+import { use, useSyncExternalStore } from 'react';
-import useMount from '../useMount';
import type { Monaco } from '../..';
+import initMonaco from './initMonaco';
function useMonaco(): Monaco | undefined {
- const [monaco, setMonaco] = useState(loader.__getMonacoInstance() as Monaco | undefined);
-
- useMount(() => {
- let cancelable: ReturnType;
-
- if (!monaco) {
- cancelable = loader.init();
-
- void cancelable
- .then((monaco) => {
- setMonaco(monaco as Monaco);
- return;
- })
- .catch((error: unknown) => {
- if ((error as { type?: unknown })?.type !== 'cancelation') {
- console.error('Monaco initialization: error:', error);
- }
- });
- }
-
- return () => cancelable?.cancel();
- });
-
- return monaco;
+ const isHydrated = useSyncExternalStore(subscribe, getClientSnapshot, getServerSnapshot);
+
+ if (!isHydrated) {
+ return undefined;
+ }
+
+ return use(initMonaco());
+}
+
+function subscribe(onStoreChange: () => void): () => void {
+ const timeoutId = setTimeout(onStoreChange, 0);
+
+ return () => {
+ clearTimeout(timeoutId);
+ };
+}
+
+function getClientSnapshot(): boolean {
+ return true;
+}
+
+function getServerSnapshot(): boolean {
+ return false;
}
export default useMonaco;
diff --git a/src/hooks/useMonaco/initMonaco.ts b/src/hooks/useMonaco/initMonaco.ts
new file mode 100644
index 0000000..fe29fc8
--- /dev/null
+++ b/src/hooks/useMonaco/initMonaco.ts
@@ -0,0 +1,12 @@
+import loader from '@willbooster/monaco-loader';
+
+import type { Monaco } from '../..';
+
+let monacoPromise: Promise | undefined;
+
+function initMonaco(): Promise {
+ monacoPromise ??= loader.init() as Promise;
+ return monacoPromise;
+}
+
+export default initMonaco;