diff --git a/.changeset/grumpy-pianos-sit.md b/.changeset/grumpy-pianos-sit.md new file mode 100644 index 0000000000..b52db34333 --- /dev/null +++ b/.changeset/grumpy-pianos-sit.md @@ -0,0 +1,5 @@ +--- +'slate-react': patch +--- + +Fix memory leaks by adding clean-up code that looks for ref resets in Editable and Text. diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index 42886a5402..7e8a2e8e41 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -147,7 +147,7 @@ export const Editable = (props: EditableProps) => { const editor = useSlate() // Rerender editor when composition status changed const [isComposing, setIsComposing] = useState(false) - const ref = useRef(null) + const ref = useRef(null) const deferredOperations = useRef([]) const { onUserInput, receivedUserInput } = useTrackUserInput() @@ -698,6 +698,23 @@ export const Editable = (props: EditableProps) => { [readOnly, propsOnDOMBeforeInput] ) + const callbackRef = useCallback( + node => { + if (node == null) { + EDITOR_TO_ELEMENT.delete(editor) + NODE_TO_ELEMENT.delete(editor) + + if (HAS_BEFORE_INPUT_SUPPORT) { + // @ts-ignore The `beforeinput` event isn't recognized. + ref.current.removeEventListener('beforeinput', onDOMBeforeInput) + } + } + + ref.current = node + }, + [ref, onDOMBeforeInput] + ) + // Attach a native DOM event handler for `beforeinput` events, because React's // built-in `onBeforeInput` is actually a leaky polyfill that doesn't expose // real `beforeinput` events sadly... (2019/11/04) @@ -845,7 +862,7 @@ export const Editable = (props: EditableProps) => { // this magic zIndex="-1" will fix it zindex={-1} suppressContentEditableWarning - ref={ref} + ref={callbackRef} style={{ ...(disableDefaultStyles ? {} diff --git a/packages/slate-react/src/components/text.tsx b/packages/slate-react/src/components/text.tsx index aec1a369b8..6c3d1f22d7 100644 --- a/packages/slate-react/src/components/text.tsx +++ b/packages/slate-react/src/components/text.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react' +import React, { useRef, useCallback } from 'react' import { Element, Range, Text as SlateText } from 'slate' import { ReactEditor, useSlateStatic } from '..' import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect' @@ -32,7 +32,7 @@ const Text = (props: { text, } = props const editor = useSlateStatic() - const ref = useRef(null) + const ref = useRef(null) const leaves = SlateText.decorations(text, decorations) const key = ReactEditor.findKey(editor, text) const children = [] @@ -54,20 +54,26 @@ const Text = (props: { } // Update element-related weak maps with the DOM element ref. - useIsomorphicLayoutEffect(() => { - const KEY_TO_ELEMENT = EDITOR_TO_KEY_TO_ELEMENT.get(editor) - if (ref.current) { - KEY_TO_ELEMENT?.set(key, ref.current) - NODE_TO_ELEMENT.set(text, ref.current) - ELEMENT_TO_NODE.set(ref.current, text) - } else { - KEY_TO_ELEMENT?.delete(key) - NODE_TO_ELEMENT.delete(text) - } - }) - + const callbackRef = useCallback( + (span: HTMLSpanElement | null) => { + const KEY_TO_ELEMENT = EDITOR_TO_KEY_TO_ELEMENT.get(editor) + if (span) { + KEY_TO_ELEMENT?.set(key, span) + NODE_TO_ELEMENT.set(text, span) + ELEMENT_TO_NODE.set(span, text) + } else { + KEY_TO_ELEMENT?.delete(key) + NODE_TO_ELEMENT.delete(text) + if (ref.current) { + ELEMENT_TO_NODE.delete(ref.current) + } + } + ref.current = span + }, + [ref, editor, key, text] + ) return ( - + {children} )