diff --git a/.changeset/popular-owls-rhyme.md b/.changeset/popular-owls-rhyme.md new file mode 100644 index 0000000000..406f652c75 --- /dev/null +++ b/.changeset/popular-owls-rhyme.md @@ -0,0 +1,7 @@ +--- +"@tiptap/react": patch +--- + +This does a shallow diff between the current options and the incoming ones to determine whether we should try to write the new options and incur a state update within the editor. + +It purposefully is not doing a full diff as several options are known to be problematic (callback handlers, extensions array, the content itself), so we rely on referential equality only to do this diffing which should be fairly fast since there are only about 10-15 options, and this diffs only the ones the user has actually attempted to set. Some options (e.g. editorProps, parseOptions, coreExtensionOptions) are an object that may need to be memoized by the user if they want to avoid unnecessary state updates. diff --git a/packages/react/src/useEditor.ts b/packages/react/src/useEditor.ts index 7768406154..9bbc1a555d 100644 --- a/packages/react/src/useEditor.ts +++ b/packages/react/src/useEditor.ts @@ -184,6 +184,33 @@ class EditorInstanceManager { } } + static compareOptions(a: UseEditorOptions, b: UseEditorOptions) { + return (Object.keys(a) as (keyof UseEditorOptions)[]).every(key => { + if (['onCreate', 'onBeforeCreate', 'onDestroy', 'onUpdate', 'onTransaction', 'onFocus', 'onBlur', 'onSelectionUpdate', 'onContentError', 'onDrop', 'onPaste'].includes(key)) { + // we don't want to compare callbacks, they are always different and only registered once + return true + } + + // We often encourage putting extensions inlined in the options object, so we will do a slightly deeper comparison here + if (key === 'extensions' && a.extensions && b.extensions) { + if (a.extensions.length !== b.extensions.length) { + return false + } + return a.extensions.every((extension, index) => { + if (extension !== b.extensions?.[index]) { + return false + } + return true + }) + } + if (a[key] !== b[key]) { + // if any of the options have changed, we should update the editor options + return false + } + return true + }) + } + /** * On each render, we will create, update, or destroy the editor instance. * @param deps The dependencies to watch for changes @@ -197,12 +224,15 @@ class EditorInstanceManager { clearTimeout(this.scheduledDestructionTimeout) if (this.editor && !this.editor.isDestroyed && 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 - this.editor.setOptions({ - ...this.options.current, - editable: this.editor.isEditable, - }) + // if the editor does exist & deps are empty, we don't need to re-initialize the editor generally + if (!EditorInstanceManager.compareOptions(this.options.current, this.editor.options)) { + // But, the options are different, so we need to update the editor options + // Still, this is faster than re-creating the editor + this.editor.setOptions({ + ...this.options.current, + editable: this.editor.isEditable, + }) + } } else { // When the editor: // - does not yet exist