@tiptap/react 2.5.9 → 3.0.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
+ import { type ArrowOptions, type AutoPlacementOptions, type FlipOptions, type HideOptions, type InlineOptions, type OffsetOptions, type Placement, type ShiftOptions, type SizeOptions, type Strategy } from '@floating-ui/dom';
1
2
  import { Editor } from '@tiptap/core';
2
3
  import { EditorState, Plugin, PluginKey } from '@tiptap/pm/state';
3
4
  import { EditorView } from '@tiptap/pm/view';
4
- import { Instance, Props } from 'tippy.js';
5
5
  export interface FloatingMenuPluginProps {
6
6
  /**
7
7
  * The plugin key for the floating menu.
@@ -18,23 +18,33 @@ export interface FloatingMenuPluginProps {
18
18
  * @default null
19
19
  */
20
20
  element: HTMLElement;
21
- /**
22
- * The options for the tippy instance.
23
- * @default {}
24
- * @see https://atomiks.github.io/tippyjs/v6/all-props/
25
- */
26
- tippyOptions?: Partial<Props>;
27
21
  /**
28
22
  * A function that determines whether the menu should be shown or not.
29
23
  * If this function returns `false`, the menu will be hidden, otherwise it will be shown.
30
- * @default null
31
24
  */
32
- shouldShow?: ((props: {
25
+ shouldShow: ((props: {
33
26
  editor: Editor;
34
27
  view: EditorView;
35
28
  state: EditorState;
36
29
  oldState?: EditorState;
30
+ from: number;
31
+ to: number;
37
32
  }) => boolean) | null;
33
+ /**
34
+ * FloatingUI options.
35
+ */
36
+ options?: {
37
+ strategy?: Strategy;
38
+ placement?: Placement;
39
+ offset?: OffsetOptions | boolean;
40
+ flip?: FlipOptions | boolean;
41
+ shift?: ShiftOptions | boolean;
42
+ arrow?: ArrowOptions | false;
43
+ size?: SizeOptions | boolean;
44
+ autoPlacement?: AutoPlacementOptions | boolean;
45
+ hide?: HideOptions | boolean;
46
+ inline?: InlineOptions | boolean;
47
+ };
38
48
  }
39
49
  export type FloatingMenuViewProps = FloatingMenuPluginProps & {
40
50
  /**
@@ -47,17 +57,22 @@ export declare class FloatingMenuView {
47
57
  element: HTMLElement;
48
58
  view: EditorView;
49
59
  preventHide: boolean;
50
- tippy: Instance | undefined;
51
- tippyOptions?: Partial<Props>;
52
60
  shouldShow: Exclude<FloatingMenuPluginProps['shouldShow'], null>;
53
- constructor({ editor, element, view, tippyOptions, shouldShow, }: FloatingMenuViewProps);
61
+ private floatingUIOptions;
62
+ get middlewares(): {
63
+ name: string;
64
+ options?: any;
65
+ fn: (state: import("@floating-ui/dom").MiddlewareState) => import("@floating-ui/core").MiddlewareReturn | Promise<import("@floating-ui/core").MiddlewareReturn>;
66
+ }[];
67
+ constructor({ editor, element, view, options, shouldShow, }: FloatingMenuViewProps);
68
+ getShouldShow(oldState?: EditorState): boolean;
69
+ updateHandler: (view: EditorView, selectionChanged: boolean, docChanged: boolean, oldState?: EditorState) => void;
54
70
  mousedownHandler: () => void;
55
71
  focusHandler: () => void;
56
72
  blurHandler: ({ event }: {
57
73
  event: FocusEvent;
58
74
  }) => void;
59
- tippyBlurHandler: (event: FocusEvent) => void;
60
- createTooltip(): void;
75
+ updatePosition(): void;
61
76
  update(view: EditorView, oldState?: EditorState): void;
62
77
  show(): void;
63
78
  hide(): void;
@@ -6,6 +6,8 @@ export type BubbleMenuProps = Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>,
6
6
  className?: string;
7
7
  children: React.ReactNode;
8
8
  updateDelay?: number;
9
+ resizeDelay?: number;
10
+ options?: BubbleMenuPluginProps['options'];
9
11
  };
10
12
  export declare const BubbleMenu: (props: BubbleMenuProps) => React.JSX.Element;
11
13
  export {};
@@ -5,6 +5,7 @@ export type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKe
5
5
  editor: FloatingMenuPluginProps['editor'] | null;
6
6
  className?: string;
7
7
  children: React.ReactNode;
8
+ options?: FloatingMenuPluginProps['options'];
8
9
  };
9
10
  export declare const FloatingMenu: (props: FloatingMenuProps) => React.JSX.Element;
10
11
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiptap/react",
3
3
  "description": "React components for tiptap",
4
- "version": "2.5.9",
4
+ "version": "3.0.0-next.0",
5
5
  "homepage": "https://tiptap.dev",
6
6
  "keywords": [
7
7
  "tiptap",
@@ -29,22 +29,22 @@
29
29
  "dist"
30
30
  ],
31
31
  "dependencies": {
32
- "@tiptap/extension-bubble-menu": "^2.5.9",
33
- "@tiptap/extension-floating-menu": "^2.5.9",
32
+ "@tiptap/extension-bubble-menu": "^3.0.0-next.0",
33
+ "@tiptap/extension-floating-menu": "^3.0.0-next.0",
34
34
  "@types/use-sync-external-store": "^0.0.6",
35
35
  "use-sync-external-store": "^1.2.2"
36
36
  },
37
37
  "devDependencies": {
38
- "@tiptap/core": "^2.5.9",
39
- "@tiptap/pm": "^2.5.9",
38
+ "@tiptap/core": "^3.0.0-next.0",
39
+ "@tiptap/pm": "^3.0.0-next.0",
40
40
  "@types/react": "^18.2.14",
41
41
  "@types/react-dom": "^18.2.6",
42
42
  "react": "^18.0.0",
43
43
  "react-dom": "^18.0.0"
44
44
  },
45
45
  "peerDependencies": {
46
- "@tiptap/core": "^2.5.9",
47
- "@tiptap/pm": "^2.5.9",
46
+ "@tiptap/core": "^3.0.0-next.0",
47
+ "@tiptap/pm": "^3.0.0-next.0",
48
48
  "react": "^17.0.0 || ^18.0.0",
49
49
  "react-dom": "^17.0.0 || ^18.0.0"
50
50
  },
@@ -1,5 +1,6 @@
1
1
  import { BubbleMenuPlugin, BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu'
2
- import React, { useEffect, useState } from 'react'
2
+ import React, { useEffect, useRef } from 'react'
3
+ import { createPortal } from 'react-dom'
3
4
 
4
5
  import { useCurrentEditor } from './Context.js'
5
6
 
@@ -10,23 +11,24 @@ export type BubbleMenuProps = Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>,
10
11
  className?: string;
11
12
  children: React.ReactNode;
12
13
  updateDelay?: number;
14
+ resizeDelay?: number;
15
+ options?: BubbleMenuPluginProps['options'];
13
16
  };
14
17
 
15
18
  export const BubbleMenu = (props: BubbleMenuProps) => {
16
- const [element, setElement] = useState<HTMLDivElement | null>(null)
19
+ const menuEl = useRef(document.createElement('div'))
17
20
  const { editor: currentEditor } = useCurrentEditor()
18
21
 
19
22
  useEffect(() => {
20
- if (!element) {
21
- return
22
- }
23
+ menuEl.current.style.visibility = 'hidden'
24
+ menuEl.current.style.position = 'absolute'
23
25
 
24
26
  if (props.editor?.isDestroyed || currentEditor?.isDestroyed) {
25
27
  return
26
28
  }
27
29
 
28
30
  const {
29
- pluginKey = 'bubbleMenu', editor, tippyOptions = {}, updateDelay, shouldShow = null,
31
+ pluginKey = 'bubbleMenu', editor, updateDelay, resizeDelay, shouldShow = null,
30
32
  } = props
31
33
 
32
34
  const menuEditor = editor || currentEditor
@@ -38,20 +40,35 @@ export const BubbleMenu = (props: BubbleMenuProps) => {
38
40
 
39
41
  const plugin = BubbleMenuPlugin({
40
42
  updateDelay,
43
+ resizeDelay,
41
44
  editor: menuEditor,
42
- element,
45
+ element: menuEl.current,
43
46
  pluginKey,
44
47
  shouldShow,
45
- tippyOptions,
48
+ options: props.options,
46
49
  })
47
50
 
48
51
  menuEditor.registerPlugin(plugin)
49
- return () => menuEditor.unregisterPlugin(pluginKey)
50
- }, [props.editor, currentEditor, element])
51
52
 
52
- return (
53
- <div ref={setElement} className={props.className} style={{ visibility: 'hidden' }}>
53
+ return () => {
54
+ menuEditor.unregisterPlugin(pluginKey)
55
+ window.requestAnimationFrame(() => {
56
+ if (menuEl.current.parentNode) {
57
+ menuEl.current.parentNode.removeChild(menuEl.current)
58
+ }
59
+ })
60
+ }
61
+ }, [props.editor, currentEditor])
62
+
63
+ const portal = createPortal(
64
+ (
65
+ <div className={props.className}>
54
66
  {props.children}
55
67
  </div>
68
+ ), menuEl.current,
69
+ )
70
+
71
+ return (
72
+ <>{portal}</>
56
73
  )
57
74
  }
@@ -1,7 +1,8 @@
1
1
  import { FloatingMenuPlugin, FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'
2
2
  import React, {
3
- useEffect, useState,
3
+ useEffect, useRef,
4
4
  } from 'react'
5
+ import { createPortal } from 'react-dom'
5
6
 
6
7
  import { useCurrentEditor } from './Context.js'
7
8
 
@@ -11,16 +12,16 @@ export type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKe
11
12
  editor: FloatingMenuPluginProps['editor'] | null;
12
13
  className?: string,
13
14
  children: React.ReactNode
15
+ options?: FloatingMenuPluginProps['options']
14
16
  }
15
17
 
16
18
  export const FloatingMenu = (props: FloatingMenuProps) => {
17
- const [element, setElement] = useState<HTMLDivElement | null>(null)
19
+ const menuEl = useRef(document.createElement('div'))
18
20
  const { editor: currentEditor } = useCurrentEditor()
19
21
 
20
22
  useEffect(() => {
21
- if (!element) {
22
- return
23
- }
23
+ menuEl.current.style.visibility = 'hidden'
24
+ menuEl.current.style.position = 'absolute'
24
25
 
25
26
  if (props.editor?.isDestroyed || currentEditor?.isDestroyed) {
26
27
  return
@@ -29,7 +30,7 @@ export const FloatingMenu = (props: FloatingMenuProps) => {
29
30
  const {
30
31
  pluginKey = 'floatingMenu',
31
32
  editor,
32
- tippyOptions = {},
33
+ options,
33
34
  shouldShow = null,
34
35
  } = props
35
36
 
@@ -43,22 +44,34 @@ export const FloatingMenu = (props: FloatingMenuProps) => {
43
44
  const plugin = FloatingMenuPlugin({
44
45
  pluginKey,
45
46
  editor: menuEditor,
46
- element,
47
- tippyOptions,
47
+ element: menuEl.current,
48
+ options,
48
49
  shouldShow,
49
50
  })
50
51
 
51
52
  menuEditor.registerPlugin(plugin)
52
- return () => menuEditor.unregisterPlugin(pluginKey)
53
+ return () => {
54
+ menuEditor.unregisterPlugin(pluginKey)
55
+ window.requestAnimationFrame(() => {
56
+ if (menuEl.current.parentNode) {
57
+ menuEl.current.parentNode.removeChild(menuEl.current)
58
+ }
59
+ })
60
+ }
53
61
  }, [
54
62
  props.editor,
55
63
  currentEditor,
56
- element,
57
64
  ])
58
65
 
59
- return (
60
- <div ref={setElement} className={props.className} style={{ visibility: 'hidden' }}>
66
+ const portal = createPortal(
67
+ (
68
+ <div className={props.className}>
61
69
  {props.children}
62
70
  </div>
71
+ ), menuEl.current,
72
+ )
73
+
74
+ return (
75
+ <>{portal}</>
63
76
  )
64
77
  }
package/src/useEditor.ts CHANGED
@@ -1,13 +1,8 @@
1
1
  import { EditorOptions } from '@tiptap/core'
2
2
  import {
3
- DependencyList,
4
- MutableRefObject,
5
- useDebugValue,
6
- useEffect,
7
- useRef,
8
- useState,
3
+ DependencyList, MutableRefObject,
4
+ useDebugValue, useEffect, useRef, useState,
9
5
  } from 'react'
10
- import { useSyncExternalStore } from 'use-sync-external-store/shim'
11
6
 
12
7
  import { Editor } from './Editor.js'
13
8
  import { useEditorState } from './useEditorState.js'
@@ -36,69 +31,55 @@ export type UseEditorOptions = Partial<EditorOptions> & {
36
31
  };
37
32
 
38
33
  /**
39
- * This class handles the creation, destruction, and re-creation of the editor instance.
34
+ * Create a new editor instance. And attach event listeners.
40
35
  */
41
- class EditorInstanceManager {
42
- /**
43
- * The current editor instance.
44
- */
45
- private editor: Editor | null = null
46
-
47
- /**
48
- * The most recent options to apply to the editor.
49
- */
50
- private options: MutableRefObject<UseEditorOptions>
51
-
52
- /**
53
- * The subscriptions to notify when the editor instance
54
- * has been created or destroyed.
55
- */
56
- private subscriptions = new Set<() => void>()
57
-
58
- /**
59
- * A timeout to destroy the editor if it was not mounted within a time frame.
60
- */
61
- private scheduledDestructionTimeout: ReturnType<typeof setTimeout> | undefined
62
-
63
- /**
64
- * Whether the editor has been mounted.
65
- */
66
- private isComponentMounted = false
67
-
68
- /**
69
- * The most recent dependencies array.
70
- */
71
- private previousDeps: DependencyList | null = null
72
-
73
- /**
74
- * The unique instance ID. This is used to identify the editor instance. And will be re-generated for each new instance.
75
- */
76
- public instanceId = ''
77
-
78
- constructor(options: MutableRefObject<UseEditorOptions>) {
79
- this.options = options
80
- this.subscriptions = new Set<() => void>()
81
- this.setEditor(this.getInitialEditor())
36
+ function createEditor(options: MutableRefObject<UseEditorOptions>): Editor {
37
+ const editor = new Editor(options.current)
38
+
39
+ editor.on('beforeCreate', (...args) => options.current.onBeforeCreate?.(...args))
40
+ editor.on('blur', (...args) => options.current.onBlur?.(...args))
41
+ editor.on('create', (...args) => options.current.onCreate?.(...args))
42
+ editor.on('destroy', (...args) => options.current.onDestroy?.(...args))
43
+ editor.on('focus', (...args) => options.current.onFocus?.(...args))
44
+ editor.on('selectionUpdate', (...args) => options.current.onSelectionUpdate?.(...args))
45
+ editor.on('transaction', (...args) => options.current.onTransaction?.(...args))
46
+ editor.on('update', (...args) => options.current.onUpdate?.(...args))
47
+ editor.on('contentError', (...args) => options.current.onContentError?.(...args))
82
48
 
83
- this.getEditor = this.getEditor.bind(this)
84
- this.getServerSnapshot = this.getServerSnapshot.bind(this)
85
- this.subscribe = this.subscribe.bind(this)
86
- this.refreshEditorInstance = this.refreshEditorInstance.bind(this)
87
- this.scheduleDestroy = this.scheduleDestroy.bind(this)
88
- this.onRender = this.onRender.bind(this)
89
- this.createEditor = this.createEditor.bind(this)
90
- }
49
+ return editor
50
+ }
91
51
 
92
- private setEditor(editor: Editor | null) {
93
- this.editor = editor
94
- this.instanceId = Math.random().toString(36).slice(2, 9)
52
+ /**
53
+ * This hook allows you to create an editor instance.
54
+ * @param options The editor options
55
+ * @param deps The dependencies to watch for changes
56
+ * @returns The editor instance
57
+ * @example const editor = useEditor({ extensions: [...] })
58
+ */
59
+ export function useEditor(
60
+ options: UseEditorOptions & { immediatelyRender: true },
61
+ deps?: DependencyList
62
+ ): Editor;
95
63
 
96
- // Notify all subscribers that the editor instance has been created
97
- this.subscriptions.forEach(cb => cb())
98
- }
64
+ /**
65
+ * This hook allows you to create an editor instance.
66
+ * @param options The editor options
67
+ * @param deps The dependencies to watch for changes
68
+ * @returns The editor instance
69
+ * @example const editor = useEditor({ extensions: [...] })
70
+ */
71
+ export function useEditor(
72
+ options?: UseEditorOptions,
73
+ deps?: DependencyList
74
+ ): Editor | null;
99
75
 
100
- private getInitialEditor() {
101
- if (this.options.current.immediatelyRender === undefined) {
76
+ export function useEditor(
77
+ options: UseEditorOptions = {},
78
+ deps: DependencyList = [],
79
+ ): Editor | null {
80
+ const mostRecentOptions = useRef(options)
81
+ const [editor, setEditor] = useState(() => {
82
+ if (options.immediatelyRender === undefined) {
102
83
  if (isSSR || isNext) {
103
84
  // TODO in the next major release, we should throw an error here
104
85
  if (isDev) {
@@ -116,205 +97,70 @@ class EditorInstanceManager {
116
97
  }
117
98
 
118
99
  // Default to immediately rendering when client-side rendering
119
- return this.createEditor()
100
+ return createEditor(mostRecentOptions)
120
101
  }
121
102
 
122
- if (this.options.current.immediatelyRender && isSSR && isDev) {
103
+ if (options.immediatelyRender && isSSR && isDev) {
123
104
  // Warn in development, to make sure the developer is aware that tiptap cannot be SSR'd, set `immediatelyRender` to `false` to avoid hydration mismatches.
124
105
  throw new Error(
125
106
  'Tiptap Error: SSR has been detected, and `immediatelyRender` has been set to `true` this is an unsupported configuration that may result in errors, explicitly set `immediatelyRender` to `false` to avoid hydration mismatches.',
126
107
  )
127
108
  }
128
109
 
129
- if (this.options.current.immediatelyRender) {
130
- return this.createEditor()
110
+ if (options.immediatelyRender) {
111
+ return createEditor(mostRecentOptions)
131
112
  }
132
113
 
133
114
  return null
134
- }
135
-
136
- /**
137
- * Create a new editor instance. And attach event listeners.
138
- */
139
- private createEditor(): Editor {
140
- const editor = new Editor(this.options.current)
141
-
142
- // Always call the most recent version of the callback function by default
143
- editor.on('beforeCreate', (...args) => this.options.current.onBeforeCreate?.(...args))
144
- editor.on('blur', (...args) => this.options.current.onBlur?.(...args))
145
- editor.on('create', (...args) => this.options.current.onCreate?.(...args))
146
- editor.on('destroy', (...args) => this.options.current.onDestroy?.(...args))
147
- editor.on('focus', (...args) => this.options.current.onFocus?.(...args))
148
- editor.on('selectionUpdate', (...args) => this.options.current.onSelectionUpdate?.(...args))
149
- editor.on('transaction', (...args) => this.options.current.onTransaction?.(...args))
150
- editor.on('update', (...args) => this.options.current.onUpdate?.(...args))
151
- editor.on('contentError', (...args) => this.options.current.onContentError?.(...args))
152
-
153
- // no need to keep track of the event listeners, they will be removed when the editor is destroyed
154
-
155
- return editor
156
- }
157
-
158
- /**
159
- * Get the current editor instance.
160
- */
161
- getEditor(): Editor | null {
162
- return this.editor
163
- }
164
-
165
- /**
166
- * Always disable the editor on the server-side.
167
- */
168
- getServerSnapshot(): null {
169
- return null
170
- }
171
-
172
- /**
173
- * Subscribe to the editor instance's changes.
174
- */
175
- subscribe(onStoreChange: () => void) {
176
- this.subscriptions.add(onStoreChange)
115
+ })
116
+ const mostRecentEditor = useRef<Editor | null>(editor)
177
117
 
178
- return () => {
179
- this.subscriptions.delete(onStoreChange)
180
- }
181
- }
118
+ mostRecentEditor.current = editor
182
119
 
183
- /**
184
- * On each render, we will create, update, or destroy the editor instance.
185
- * @param deps The dependencies to watch for changes
186
- * @returns A cleanup function
187
- */
188
- onRender(deps: DependencyList) {
189
- // The returned callback will run on each render
190
- return () => {
191
- this.isComponentMounted = true
192
- // Cleanup any scheduled destructions, since we are currently rendering
193
- clearTimeout(this.scheduledDestructionTimeout)
194
-
195
- if (this.editor && !this.editor.isDestroyed && deps.length === 0) {
196
- // if the editor does exist & deps are empty, we don't need to re-initialize the editor
197
- // we can fast-path to update the editor options on the existing instance
198
- this.editor.setOptions(this.options.current)
199
- } else {
200
- // When the editor:
201
- // - does not yet exist
202
- // - is destroyed
203
- // - the deps array changes
204
- // We need to destroy the editor instance and re-initialize it
205
- this.refreshEditorInstance(deps)
206
- }
120
+ useDebugValue(editor)
207
121
 
208
- return () => {
209
- this.isComponentMounted = false
210
- this.scheduleDestroy()
122
+ // This effect will handle creating/updating the editor instance
123
+ useEffect(() => {
124
+ const destroyUnusedEditor = (editorInstance: Editor | null) => {
125
+ if (editorInstance) {
126
+ // We need to destroy the editor asynchronously to avoid memory leaks
127
+ // because the editor instance is still being used in the component.
128
+
129
+ setTimeout(() => {
130
+ // re-use the editor instance if it hasn't been replaced yet
131
+ // otherwise, asynchronously destroy the old editor instance
132
+ if (editorInstance !== mostRecentEditor.current && !editorInstance.isDestroyed) {
133
+ editorInstance.destroy()
134
+ }
135
+ })
211
136
  }
212
137
  }
213
- }
214
-
215
- /**
216
- * Recreate the editor instance if the dependencies have changed.
217
- */
218
- private refreshEditorInstance(deps: DependencyList) {
219
138
 
220
- if (this.editor && !this.editor.isDestroyed) {
221
- // Editor instance already exists
222
- if (this.previousDeps === null) {
223
- // If lastDeps has not yet been initialized, reuse the current editor instance
224
- this.previousDeps = deps
225
- return
226
- }
227
- const depsAreEqual = this.previousDeps.length === deps.length
228
- && this.previousDeps.every((dep, index) => dep === deps[index])
229
-
230
- if (depsAreEqual) {
231
- // deps exist and are equal, no need to recreate
232
- return
233
- }
234
- }
139
+ let editorInstance = mostRecentEditor.current
235
140
 
236
- if (this.editor && !this.editor.isDestroyed) {
237
- // Destroy the editor instance if it exists
238
- this.editor.destroy()
141
+ if (!editorInstance) {
142
+ editorInstance = createEditor(mostRecentOptions)
143
+ setEditor(editorInstance)
144
+ return () => destroyUnusedEditor(editorInstance)
239
145
  }
240
146
 
241
- this.setEditor(this.createEditor())
242
-
243
- // Update the lastDeps to the current deps
244
- this.previousDeps = deps
245
- }
246
-
247
- /**
248
- * Schedule the destruction of the editor instance.
249
- * This will only destroy the editor if it was not mounted on the next tick.
250
- * This is to avoid destroying the editor instance when it's actually still mounted.
251
- */
252
- private scheduleDestroy() {
253
- const currentInstanceId = this.instanceId
254
- const currentEditor = this.editor
255
-
256
- // Wait a tick to see if the component is still mounted
257
- this.scheduledDestructionTimeout = setTimeout(() => {
258
- if (this.isComponentMounted && this.instanceId === currentInstanceId) {
259
- // If still mounted on the next tick, with the same instanceId, do not destroy the editor
260
- if (currentEditor) {
261
- // just re-apply options as they might have changed
262
- currentEditor.setOptions(this.options.current)
263
- }
264
- return
265
- }
266
- if (currentEditor && !currentEditor.isDestroyed) {
267
- currentEditor.destroy()
268
- if (this.instanceId === currentInstanceId) {
269
- this.setEditor(null)
270
- }
271
- }
272
- }, 0)
273
- }
274
- }
275
-
276
- /**
277
- * This hook allows you to create an editor instance.
278
- * @param options The editor options
279
- * @param deps The dependencies to watch for changes
280
- * @returns The editor instance
281
- * @example const editor = useEditor({ extensions: [...] })
282
- */
283
- export function useEditor(
284
- options: UseEditorOptions & { immediatelyRender: true },
285
- deps?: DependencyList
286
- ): Editor;
287
-
288
- /**
289
- * This hook allows you to create an editor instance.
290
- * @param options The editor options
291
- * @param deps The dependencies to watch for changes
292
- * @returns The editor instance
293
- * @example const editor = useEditor({ extensions: [...] })
294
- */
295
- export function useEditor(options?: UseEditorOptions, deps?: DependencyList): Editor | null;
147
+ if (!Array.isArray(deps) || deps.length === 0) {
148
+ // if the editor does exist & deps are empty, we don't need to re-initialize the editor
149
+ // we can fast-path to update the editor options on the existing instance
150
+ editorInstance.setOptions(options)
296
151
 
297
- export function useEditor(
298
- options: UseEditorOptions = {},
299
- deps: DependencyList = [],
300
- ): Editor | null {
301
- const mostRecentOptions = useRef(options)
302
-
303
- mostRecentOptions.current = options
304
-
305
- const [instanceManager] = useState(() => new EditorInstanceManager(mostRecentOptions))
306
-
307
- const editor = useSyncExternalStore(
308
- instanceManager.subscribe,
309
- instanceManager.getEditor,
310
- instanceManager.getServerSnapshot,
311
- )
152
+ return () => destroyUnusedEditor(editorInstance)
153
+ }
312
154
 
313
- useDebugValue(editor)
155
+ // We need to destroy the editor instance and re-initialize it
156
+ // when the deps array changes
157
+ editorInstance.destroy()
314
158
 
315
- // This effect will handle creating/updating the editor instance
316
- // eslint-disable-next-line react-hooks/exhaustive-deps
317
- useEffect(instanceManager.onRender(deps))
159
+ // the deps array is used to re-initialize the editor instance
160
+ editorInstance = createEditor(mostRecentOptions)
161
+ setEditor(editorInstance)
162
+ return () => destroyUnusedEditor(editorInstance)
163
+ }, deps)
318
164
 
319
165
  // The default behavior is to re-render on each transaction
320
166
  // This is legacy behavior that will be removed in future versions