mirror of
https://github.com/ueberdosis/tiptap.git
synced 2025-06-12 04:33:34 +08:00
fix(react): always cleanup old editor instances (#5414)
This commit is contained in:
parent
ce141c2e69
commit
99d7820cbb
5
.changeset/red-suns-allow.md
Normal file
5
.changeset/red-suns-allow.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tiptap/react": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Resolve a bug an editor could be instantiated but not destroyed. This was causing issues with multiple instances of plugins still being active and interfering with each other
|
@ -26,6 +26,9 @@ function EditorInstance({ shouldOptimizeRendering }) {
|
|||||||
A highly optimized editor that only re-renders when it’s necessary.
|
A highly optimized editor that only re-renders when it’s necessary.
|
||||||
</p>
|
</p>
|
||||||
`,
|
`,
|
||||||
|
onBeforeCreate: () => {
|
||||||
|
console.log('onBeforeCreate')
|
||||||
|
},
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* This hook allows us to select the editor state we want to use in our component.
|
* This hook allows us to select the editor state we want to use in our component.
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { EditorOptions } from '@tiptap/core'
|
import { EditorOptions } from '@tiptap/core'
|
||||||
import {
|
import {
|
||||||
DependencyList, useDebugValue, useEffect, useRef, useState,
|
DependencyList, MutableRefObject,
|
||||||
|
useDebugValue, useEffect, useRef, useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
|
||||||
import { Editor } from './Editor.js'
|
import { Editor } from './Editor.js'
|
||||||
@ -29,6 +30,25 @@ export type UseEditorOptions = Partial<EditorOptions> & {
|
|||||||
shouldRerenderOnTransaction?: boolean;
|
shouldRerenderOnTransaction?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new editor instance. And attach event listeners.
|
||||||
|
*/
|
||||||
|
function createEditor(options: MutableRefObject<UseEditorOptions>): Editor {
|
||||||
|
const editor = new Editor(options.current)
|
||||||
|
|
||||||
|
editor.on('beforeCreate', (...args) => options.current.onBeforeCreate?.(...args))
|
||||||
|
editor.on('blur', (...args) => options.current.onBlur?.(...args))
|
||||||
|
editor.on('create', (...args) => options.current.onCreate?.(...args))
|
||||||
|
editor.on('destroy', (...args) => options.current.onDestroy?.(...args))
|
||||||
|
editor.on('focus', (...args) => options.current.onFocus?.(...args))
|
||||||
|
editor.on('selectionUpdate', (...args) => options.current.onSelectionUpdate?.(...args))
|
||||||
|
editor.on('transaction', (...args) => options.current.onTransaction?.(...args))
|
||||||
|
editor.on('update', (...args) => options.current.onUpdate?.(...args))
|
||||||
|
editor.on('contentError', (...args) => options.current.onContentError?.(...args))
|
||||||
|
|
||||||
|
return editor
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This hook allows you to create an editor instance.
|
* This hook allows you to create an editor instance.
|
||||||
* @param options The editor options
|
* @param options The editor options
|
||||||
@ -57,7 +77,7 @@ export function useEditor(
|
|||||||
options: UseEditorOptions = {},
|
options: UseEditorOptions = {},
|
||||||
deps: DependencyList = [],
|
deps: DependencyList = [],
|
||||||
): Editor | null {
|
): Editor | null {
|
||||||
const isMounted = useRef(false)
|
const mostRecentOptions = useRef(options)
|
||||||
const [editor, setEditor] = useState(() => {
|
const [editor, setEditor] = useState(() => {
|
||||||
if (options.immediatelyRender === undefined) {
|
if (options.immediatelyRender === undefined) {
|
||||||
if (isSSR || isNext) {
|
if (isSSR || isNext) {
|
||||||
@ -77,7 +97,7 @@ export function useEditor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default to immediately rendering when client-side rendering
|
// Default to immediately rendering when client-side rendering
|
||||||
return new Editor(options)
|
return createEditor(mostRecentOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.immediatelyRender && isSSR && isDev) {
|
if (options.immediatelyRender && isSSR && isDev) {
|
||||||
@ -88,167 +108,59 @@ export function useEditor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.immediatelyRender) {
|
if (options.immediatelyRender) {
|
||||||
return new Editor(options)
|
return createEditor(mostRecentOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
const mostRecentEditor = useRef<Editor | null>(editor)
|
||||||
|
|
||||||
|
mostRecentEditor.current = editor
|
||||||
|
|
||||||
useDebugValue(editor)
|
useDebugValue(editor)
|
||||||
|
|
||||||
// This effect will handle creating/updating the editor instance
|
// This effect will handle creating/updating the editor instance
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let editorInstance: Editor | null = editor
|
const destroyUnusedEditor = (editorInstance: Editor | null) => {
|
||||||
|
if (editorInstance) {
|
||||||
if (!editorInstance) {
|
|
||||||
editorInstance = new Editor(options)
|
|
||||||
// instantiate the editor if it doesn't exist
|
|
||||||
// for ssr, this is the first time the editor is created
|
|
||||||
setEditor(editorInstance)
|
|
||||||
} else if (Array.isArray(deps) && deps.length) {
|
|
||||||
// We need to destroy the editor instance and re-initialize it
|
|
||||||
// when the deps array changes
|
|
||||||
editorInstance.destroy()
|
|
||||||
|
|
||||||
// the deps array is used to re-initialize the editor instance
|
|
||||||
editorInstance = new Editor(options)
|
|
||||||
|
|
||||||
setEditor(editorInstance)
|
|
||||||
} else {
|
|
||||||
// if the editor does exist & deps are empty, we don't need to re-initialize the editor
|
|
||||||
// we can fast-path to update the editor options on the existing instance
|
|
||||||
editorInstance.setOptions(options)
|
|
||||||
}
|
|
||||||
}, deps)
|
|
||||||
|
|
||||||
const {
|
|
||||||
onBeforeCreate,
|
|
||||||
onBlur,
|
|
||||||
onCreate,
|
|
||||||
onDestroy,
|
|
||||||
onFocus,
|
|
||||||
onSelectionUpdate,
|
|
||||||
onTransaction,
|
|
||||||
onUpdate,
|
|
||||||
onContentError,
|
|
||||||
} = options
|
|
||||||
|
|
||||||
const onBeforeCreateRef = useRef(onBeforeCreate)
|
|
||||||
const onBlurRef = useRef(onBlur)
|
|
||||||
const onCreateRef = useRef(onCreate)
|
|
||||||
const onDestroyRef = useRef(onDestroy)
|
|
||||||
const onFocusRef = useRef(onFocus)
|
|
||||||
const onSelectionUpdateRef = useRef(onSelectionUpdate)
|
|
||||||
const onTransactionRef = useRef(onTransaction)
|
|
||||||
const onUpdateRef = useRef(onUpdate)
|
|
||||||
const onContentErrorRef = useRef(onContentError)
|
|
||||||
|
|
||||||
// This effect will handle updating the editor instance
|
|
||||||
// when the event handlers change.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!editor) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onBeforeCreate) {
|
|
||||||
editor.off('beforeCreate', onBeforeCreateRef.current)
|
|
||||||
editor.on('beforeCreate', onBeforeCreate)
|
|
||||||
|
|
||||||
onBeforeCreateRef.current = onBeforeCreate
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onBlur) {
|
|
||||||
editor.off('blur', onBlurRef.current)
|
|
||||||
editor.on('blur', onBlur)
|
|
||||||
|
|
||||||
onBlurRef.current = onBlur
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onCreate) {
|
|
||||||
editor.off('create', onCreateRef.current)
|
|
||||||
editor.on('create', onCreate)
|
|
||||||
|
|
||||||
onCreateRef.current = onCreate
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onDestroy) {
|
|
||||||
editor.off('destroy', onDestroyRef.current)
|
|
||||||
editor.on('destroy', onDestroy)
|
|
||||||
|
|
||||||
onDestroyRef.current = onDestroy
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onFocus) {
|
|
||||||
editor.off('focus', onFocusRef.current)
|
|
||||||
editor.on('focus', onFocus)
|
|
||||||
|
|
||||||
onFocusRef.current = onFocus
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onSelectionUpdate) {
|
|
||||||
editor.off('selectionUpdate', onSelectionUpdateRef.current)
|
|
||||||
editor.on('selectionUpdate', onSelectionUpdate)
|
|
||||||
|
|
||||||
onSelectionUpdateRef.current = onSelectionUpdate
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onTransaction) {
|
|
||||||
editor.off('transaction', onTransactionRef.current)
|
|
||||||
editor.on('transaction', onTransaction)
|
|
||||||
|
|
||||||
onTransactionRef.current = onTransaction
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onUpdate) {
|
|
||||||
editor.off('update', onUpdateRef.current)
|
|
||||||
editor.on('update', onUpdate)
|
|
||||||
|
|
||||||
onUpdateRef.current = onUpdate
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onContentError) {
|
|
||||||
editor.off('contentError', onContentErrorRef.current)
|
|
||||||
editor.on('contentError', onContentError)
|
|
||||||
|
|
||||||
onContentErrorRef.current = onContentError
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
onBeforeCreate,
|
|
||||||
onBlur,
|
|
||||||
onCreate,
|
|
||||||
onDestroy,
|
|
||||||
onFocus,
|
|
||||||
onSelectionUpdate,
|
|
||||||
onTransaction,
|
|
||||||
onUpdate,
|
|
||||||
onContentError,
|
|
||||||
editor,
|
|
||||||
])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy the editor instance when the component completely unmounts
|
|
||||||
* As opposed to the cleanup function in the effect above, this will
|
|
||||||
* only be called when the component is removed from the DOM, since it has no deps.
|
|
||||||
* */
|
|
||||||
useEffect(() => {
|
|
||||||
isMounted.current = true
|
|
||||||
return () => {
|
|
||||||
isMounted.current = false
|
|
||||||
if (editor) {
|
|
||||||
// We need to destroy the editor asynchronously to avoid memory leaks
|
// We need to destroy the editor asynchronously to avoid memory leaks
|
||||||
// because the editor instance is still being used in the component.
|
// because the editor instance is still being used in the component.
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// re-use the editor instance if it hasn't been destroyed yet
|
// re-use the editor instance if it hasn't been replaced yet
|
||||||
// and the component is still mounted
|
// otherwise, asynchronously destroy the old editor instance
|
||||||
// otherwise, asynchronously destroy the editor instance
|
if (editorInstance !== mostRecentEditor.current && !editorInstance.isDestroyed) {
|
||||||
if (!isMounted.current && !editor.isDestroyed) {
|
editorInstance.destroy()
|
||||||
editor.destroy()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
|
||||||
|
let editorInstance = mostRecentEditor.current
|
||||||
|
|
||||||
|
if (!editorInstance) {
|
||||||
|
editorInstance = createEditor(mostRecentOptions)
|
||||||
|
setEditor(editorInstance)
|
||||||
|
return () => destroyUnusedEditor(editorInstance)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(deps) || deps.length === 0) {
|
||||||
|
// if the editor does exist & deps are empty, we don't need to re-initialize the editor
|
||||||
|
// we can fast-path to update the editor options on the existing instance
|
||||||
|
editorInstance.setOptions(options)
|
||||||
|
|
||||||
|
return () => destroyUnusedEditor(editorInstance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to destroy the editor instance and re-initialize it
|
||||||
|
// when the deps array changes
|
||||||
|
editorInstance.destroy()
|
||||||
|
|
||||||
|
// the deps array is used to re-initialize the editor instance
|
||||||
|
editorInstance = createEditor(mostRecentOptions)
|
||||||
|
setEditor(editorInstance)
|
||||||
|
return () => destroyUnusedEditor(editorInstance)
|
||||||
|
}, deps)
|
||||||
|
|
||||||
// The default behavior is to re-render on each transaction
|
// The default behavior is to re-render on each transaction
|
||||||
// This is legacy behavior that will be removed in future versions
|
// This is legacy behavior that will be removed in future versions
|
||||||
|
Loading…
Reference in New Issue
Block a user