perf(react): do some light diffing to not reset options on every render #6024 (#6031)
Some checks failed
build / lint (20) (push) Has been cancelled
build / test (20, map[name:Demos/Examples spec:./demos/src/Examples/**/*.spec.{js,ts}]) (push) Has been cancelled
build / test (20, map[name:Demos/Experiments spec:./demos/src/Experiments/**/*.spec.{js,ts}]) (push) Has been cancelled
build / test (20, map[name:Demos/Extensions spec:./demos/src/Extensions/**/*.spec.{js,ts}]) (push) Has been cancelled
build / test (20, map[name:Demos/GuideContent spec:./demos/src/GuideContent/**/*.spec.{js,ts}]) (push) Has been cancelled
build / test (20, map[name:Demos/GuideGettingStarted spec:./demos/src/GuideGettingStarted/**/*.spec.{js,ts}]) (push) Has been cancelled
build / test (20, map[name:Demos/Marks spec:./demos/src/Marks/**/*.spec.{js,ts}]) (push) Has been cancelled
build / test (20, map[name:Demos/Nodes spec:./demos/src/Nodes/**/*.spec.{js,ts}]) (push) Has been cancelled
build / test (20, map[name:Integration spec:./tests/cypress/integration/**/*.spec.{js,ts}]) (push) Has been cancelled
Publish / Release (20) (push) Has been cancelled
build / build (20) (push) Has been cancelled

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.
This commit is contained in:
Nick Perez 2025-01-19 11:46:37 +01:00 committed by GitHub
parent 839eb476e0
commit 40b7d474c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 43 additions and 6 deletions

View File

@ -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.

View File

@ -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