Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(react): do some light diffing to not reset options on every render #6024 #6031

Merged
merged 2 commits into from
Jan 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading