Skip to content

Commit

Permalink
perf(react): do some light diffing to not reset options on every render
Browse files Browse the repository at this point in the history
#6024 (#6031)

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.
  • Loading branch information
nperez0111 authored Jan 19, 2025
1 parent 839eb47 commit 40b7d47
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 6 deletions.
7 changes: 7 additions & 0 deletions .changeset/popular-owls-rhyme.md
Original file line number Diff line number Diff line change
@@ -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.
42 changes: 36 additions & 6 deletions packages/react/src/useEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 40b7d47

Please sign in to comment.