@tiptap/react 2.5.7 → 2.5.8

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.
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.7",
4
+ "version": "2.5.8",
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.7",
33
- "@tiptap/extension-floating-menu": "^2.5.7",
32
+ "@tiptap/extension-bubble-menu": "^2.5.8",
33
+ "@tiptap/extension-floating-menu": "^2.5.8",
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.7",
39
- "@tiptap/pm": "^2.5.7",
38
+ "@tiptap/core": "^2.5.8",
39
+ "@tiptap/pm": "^2.5.8",
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.7",
47
- "@tiptap/pm": "^2.5.7",
46
+ "@tiptap/core": "^2.5.8",
47
+ "@tiptap/pm": "^2.5.8",
48
48
  "react": "^17.0.0 || ^18.0.0",
49
49
  "react-dom": "^17.0.0 || ^18.0.0"
50
50
  },
package/src/useEditor.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { EditorOptions } from '@tiptap/core'
2
2
  import {
3
- DependencyList, useDebugValue, useEffect, useRef, useState,
3
+ DependencyList, MutableRefObject,
4
+ useDebugValue, useEffect, useRef, useState,
4
5
  } from 'react'
5
6
 
6
7
  import { Editor } from './Editor.js'
@@ -29,6 +30,25 @@ export type UseEditorOptions = Partial<EditorOptions> & {
29
30
  shouldRerenderOnTransaction?: boolean;
30
31
  };
31
32
 
33
+ /**
34
+ * Create a new editor instance. And attach event listeners.
35
+ */
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))
48
+
49
+ return editor
50
+ }
51
+
32
52
  /**
33
53
  * This hook allows you to create an editor instance.
34
54
  * @param options The editor options
@@ -57,7 +77,7 @@ export function useEditor(
57
77
  options: UseEditorOptions = {},
58
78
  deps: DependencyList = [],
59
79
  ): Editor | null {
60
- const isMounted = useRef(false)
80
+ const mostRecentOptions = useRef(options)
61
81
  const [editor, setEditor] = useState(() => {
62
82
  if (options.immediatelyRender === undefined) {
63
83
  if (isSSR || isNext) {
@@ -77,7 +97,7 @@ export function useEditor(
77
97
  }
78
98
 
79
99
  // Default to immediately rendering when client-side rendering
80
- return new Editor(options)
100
+ return createEditor(mostRecentOptions)
81
101
  }
82
102
 
83
103
  if (options.immediatelyRender && isSSR && isDev) {
@@ -88,167 +108,59 @@ export function useEditor(
88
108
  }
89
109
 
90
110
  if (options.immediatelyRender) {
91
- return new Editor(options)
111
+ return createEditor(mostRecentOptions)
92
112
  }
93
113
 
94
114
  return null
95
115
  })
116
+ const mostRecentEditor = useRef<Editor | null>(editor)
117
+
118
+ mostRecentEditor.current = editor
96
119
 
97
120
  useDebugValue(editor)
98
121
 
99
122
  // This effect will handle creating/updating the editor instance
100
123
  useEffect(() => {
101
- let editorInstance: Editor | null = editor
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.
102
128
 
103
- if (!editorInstance) {
104
- editorInstance = new Editor(options)
105
- // instantiate the editor if it doesn't exist
106
- // for ssr, this is the first time the editor is created
107
- setEditor(editorInstance)
108
- } else if (Array.isArray(deps) && deps.length) {
109
- // We need to destroy the editor instance and re-initialize it
110
- // when the deps array changes
111
- editorInstance.destroy()
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
+ })
136
+ }
137
+ }
112
138
 
113
- // the deps array is used to re-initialize the editor instance
114
- editorInstance = new Editor(options)
139
+ let editorInstance = mostRecentEditor.current
115
140
 
141
+ if (!editorInstance) {
142
+ editorInstance = createEditor(mostRecentOptions)
116
143
  setEditor(editorInstance)
117
- } else {
144
+ return () => destroyUnusedEditor(editorInstance)
145
+ }
146
+
147
+ if (!Array.isArray(deps) || deps.length === 0) {
118
148
  // if the editor does exist & deps are empty, we don't need to re-initialize the editor
119
149
  // we can fast-path to update the editor options on the existing instance
120
150
  editorInstance.setOptions(options)
121
- }
122
- }, deps)
123
-
124
- const {
125
- onBeforeCreate,
126
- onBlur,
127
- onCreate,
128
- onDestroy,
129
- onFocus,
130
- onSelectionUpdate,
131
- onTransaction,
132
- onUpdate,
133
- onContentError,
134
- } = options
135
-
136
- const onBeforeCreateRef = useRef(onBeforeCreate)
137
- const onBlurRef = useRef(onBlur)
138
- const onCreateRef = useRef(onCreate)
139
- const onDestroyRef = useRef(onDestroy)
140
- const onFocusRef = useRef(onFocus)
141
- const onSelectionUpdateRef = useRef(onSelectionUpdate)
142
- const onTransactionRef = useRef(onTransaction)
143
- const onUpdateRef = useRef(onUpdate)
144
- const onContentErrorRef = useRef(onContentError)
145
-
146
- // This effect will handle updating the editor instance
147
- // when the event handlers change.
148
- useEffect(() => {
149
- if (!editor) {
150
- return
151
- }
152
-
153
- if (onBeforeCreate) {
154
- editor.off('beforeCreate', onBeforeCreateRef.current)
155
- editor.on('beforeCreate', onBeforeCreate)
156
-
157
- onBeforeCreateRef.current = onBeforeCreate
158
- }
159
-
160
- if (onBlur) {
161
- editor.off('blur', onBlurRef.current)
162
- editor.on('blur', onBlur)
163
-
164
- onBlurRef.current = onBlur
165
- }
166
-
167
- if (onCreate) {
168
- editor.off('create', onCreateRef.current)
169
- editor.on('create', onCreate)
170
-
171
- onCreateRef.current = onCreate
172
- }
173
-
174
- if (onDestroy) {
175
- editor.off('destroy', onDestroyRef.current)
176
- editor.on('destroy', onDestroy)
177
-
178
- onDestroyRef.current = onDestroy
179
- }
180
-
181
- if (onFocus) {
182
- editor.off('focus', onFocusRef.current)
183
- editor.on('focus', onFocus)
184
151
 
185
- onFocusRef.current = onFocus
152
+ return () => destroyUnusedEditor(editorInstance)
186
153
  }
187
154
 
188
- if (onSelectionUpdate) {
189
- editor.off('selectionUpdate', onSelectionUpdateRef.current)
190
- editor.on('selectionUpdate', onSelectionUpdate)
191
-
192
- onSelectionUpdateRef.current = onSelectionUpdate
193
- }
194
-
195
- if (onTransaction) {
196
- editor.off('transaction', onTransactionRef.current)
197
- editor.on('transaction', onTransaction)
198
-
199
- onTransactionRef.current = onTransaction
200
- }
201
-
202
- if (onUpdate) {
203
- editor.off('update', onUpdateRef.current)
204
- editor.on('update', onUpdate)
205
-
206
- onUpdateRef.current = onUpdate
207
- }
155
+ // We need to destroy the editor instance and re-initialize it
156
+ // when the deps array changes
157
+ editorInstance.destroy()
208
158
 
209
- if (onContentError) {
210
- editor.off('contentError', onContentErrorRef.current)
211
- editor.on('contentError', onContentError)
212
-
213
- onContentErrorRef.current = onContentError
214
- }
215
- }, [
216
- onBeforeCreate,
217
- onBlur,
218
- onCreate,
219
- onDestroy,
220
- onFocus,
221
- onSelectionUpdate,
222
- onTransaction,
223
- onUpdate,
224
- onContentError,
225
- editor,
226
- ])
227
-
228
- /**
229
- * Destroy the editor instance when the component completely unmounts
230
- * As opposed to the cleanup function in the effect above, this will
231
- * only be called when the component is removed from the DOM, since it has no deps.
232
- * */
233
- useEffect(() => {
234
- isMounted.current = true
235
- return () => {
236
- isMounted.current = false
237
- if (editor) {
238
- // We need to destroy the editor asynchronously to avoid memory leaks
239
- // because the editor instance is still being used in the component.
240
-
241
- setTimeout(() => {
242
- // re-use the editor instance if it hasn't been destroyed yet
243
- // and the component is still mounted
244
- // otherwise, asynchronously destroy the editor instance
245
- if (!isMounted.current && !editor.isDestroyed) {
246
- editor.destroy()
247
- }
248
- })
249
- }
250
- }
251
- }, [])
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)
252
164
 
253
165
  // The default behavior is to re-render on each transaction
254
166
  // This is legacy behavior that will be removed in future versions