@tiptap/react 2.5.6 → 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.6",
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.6",
33
- "@tiptap/extension-floating-menu": "^2.5.6",
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.6",
39
- "@tiptap/pm": "^2.5.6",
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.6",
47
- "@tiptap/pm": "^2.5.6",
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,163 +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.
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
+ })
136
+ }
137
+ }
138
+
139
+ let editorInstance = mostRecentEditor.current
102
140
 
103
141
  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
142
+ editorInstance = createEditor(mostRecentOptions)
107
143
  setEditor(editorInstance)
108
- } else if (Array.isArray(deps) && deps.length) {
109
- // the deps array is used to re-initialize the editor instance
110
- editorInstance = new Editor(options)
144
+ return () => destroyUnusedEditor(editorInstance)
145
+ }
111
146
 
112
- setEditor(editorInstance)
113
- } else {
147
+ if (!Array.isArray(deps) || deps.length === 0) {
114
148
  // if the editor does exist & deps are empty, we don't need to re-initialize the editor
115
149
  // we can fast-path to update the editor options on the existing instance
116
150
  editorInstance.setOptions(options)
117
- }
118
- }, deps)
119
151
 
120
- const {
121
- onBeforeCreate,
122
- onBlur,
123
- onCreate,
124
- onDestroy,
125
- onFocus,
126
- onSelectionUpdate,
127
- onTransaction,
128
- onUpdate,
129
- onContentError,
130
- } = options
131
-
132
- const onBeforeCreateRef = useRef(onBeforeCreate)
133
- const onBlurRef = useRef(onBlur)
134
- const onCreateRef = useRef(onCreate)
135
- const onDestroyRef = useRef(onDestroy)
136
- const onFocusRef = useRef(onFocus)
137
- const onSelectionUpdateRef = useRef(onSelectionUpdate)
138
- const onTransactionRef = useRef(onTransaction)
139
- const onUpdateRef = useRef(onUpdate)
140
- const onContentErrorRef = useRef(onContentError)
141
-
142
- // This effect will handle updating the editor instance
143
- // when the event handlers change.
144
- useEffect(() => {
145
- if (!editor) {
146
- return
152
+ return () => destroyUnusedEditor(editorInstance)
147
153
  }
148
154
 
149
- if (onBeforeCreate) {
150
- editor.off('beforeCreate', onBeforeCreateRef.current)
151
- editor.on('beforeCreate', onBeforeCreate)
152
-
153
- onBeforeCreateRef.current = onBeforeCreate
154
- }
155
-
156
- if (onBlur) {
157
- editor.off('blur', onBlurRef.current)
158
- editor.on('blur', onBlur)
159
-
160
- onBlurRef.current = onBlur
161
- }
162
-
163
- if (onCreate) {
164
- editor.off('create', onCreateRef.current)
165
- editor.on('create', onCreate)
166
-
167
- onCreateRef.current = onCreate
168
- }
155
+ // We need to destroy the editor instance and re-initialize it
156
+ // when the deps array changes
157
+ editorInstance.destroy()
169
158
 
170
- if (onDestroy) {
171
- editor.off('destroy', onDestroyRef.current)
172
- editor.on('destroy', onDestroy)
173
-
174
- onDestroyRef.current = onDestroy
175
- }
176
-
177
- if (onFocus) {
178
- editor.off('focus', onFocusRef.current)
179
- editor.on('focus', onFocus)
180
-
181
- onFocusRef.current = onFocus
182
- }
183
-
184
- if (onSelectionUpdate) {
185
- editor.off('selectionUpdate', onSelectionUpdateRef.current)
186
- editor.on('selectionUpdate', onSelectionUpdate)
187
-
188
- onSelectionUpdateRef.current = onSelectionUpdate
189
- }
190
-
191
- if (onTransaction) {
192
- editor.off('transaction', onTransactionRef.current)
193
- editor.on('transaction', onTransaction)
194
-
195
- onTransactionRef.current = onTransaction
196
- }
197
-
198
- if (onUpdate) {
199
- editor.off('update', onUpdateRef.current)
200
- editor.on('update', onUpdate)
201
-
202
- onUpdateRef.current = onUpdate
203
- }
204
-
205
- if (onContentError) {
206
- editor.off('contentError', onContentErrorRef.current)
207
- editor.on('contentError', onContentError)
208
-
209
- onContentErrorRef.current = onContentError
210
- }
211
- }, [
212
- onBeforeCreate,
213
- onBlur,
214
- onCreate,
215
- onDestroy,
216
- onFocus,
217
- onSelectionUpdate,
218
- onTransaction,
219
- onUpdate,
220
- onContentError,
221
- editor,
222
- ])
223
-
224
- /**
225
- * Destroy the editor instance when the component completely unmounts
226
- * As opposed to the cleanup function in the effect above, this will
227
- * only be called when the component is removed from the DOM, since it has no deps.
228
- * */
229
- useEffect(() => {
230
- isMounted.current = true
231
- return () => {
232
- isMounted.current = false
233
- if (editor) {
234
- // We need to destroy the editor asynchronously to avoid memory leaks
235
- // because the editor instance is still being used in the component.
236
-
237
- setTimeout(() => {
238
- // re-use the editor instance if it hasn't been destroyed yet
239
- // and the component is still mounted
240
- // otherwise, asynchronously destroy the editor instance
241
- if (!isMounted.current && !editor.isDestroyed) {
242
- editor.destroy()
243
- }
244
- })
245
- }
246
- }
247
- }, [])
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)
248
164
 
249
165
  // The default behavior is to re-render on each transaction
250
166
  // This is legacy behavior that will be removed in future versions