diff --git a/packages/slate-react/src/components/element.tsx b/packages/slate-react/src/components/element.tsx index 2a54737ac..5d641ec49 100644 --- a/packages/slate-react/src/components/element.tsx +++ b/packages/slate-react/src/components/element.tsx @@ -14,6 +14,7 @@ import { NODE_TO_INDEX, KEY_TO_ELEMENT, } from '../utils/weak-maps' +import { isDecoratorRangeListEqual } from '../utils/range-list' import { RenderElementProps, RenderLeafProps } from './editable' /** @@ -134,7 +135,7 @@ const MemoizedElement = React.memo(Element, (prev, next) => { prev.element === next.element && prev.renderElement === next.renderElement && prev.renderLeaf === next.renderLeaf && - isRangeListEqual(prev.decorations, next.decorations) && + isDecoratorRangeListEqual(prev.decorations, next.decorations) && (prev.selection === next.selection || (!!prev.selection && !!next.selection && @@ -157,29 +158,4 @@ export const DefaultElement = (props: RenderElementProps) => { ) } -/** - * Check if a list of ranges is equal to another. - * - * PERF: this requires the two lists to also have the ranges inside them in the - * same order, but this is an okay constraint for us since decorations are - * kept in order, and the odd case where they aren't is okay to re-render for. - */ - -const isRangeListEqual = (list: Range[], another: Range[]): boolean => { - if (list.length !== another.length) { - return false - } - - for (let i = 0; i < list.length; i++) { - const range = list[i] - const other = another[i] - - if (!Range.equals(range, other)) { - return false - } - } - - return true -} - export default MemoizedElement diff --git a/packages/slate-react/src/components/leaf.tsx b/packages/slate-react/src/components/leaf.tsx index a6e6f86f5..417ea797b 100644 --- a/packages/slate-react/src/components/leaf.tsx +++ b/packages/slate-react/src/components/leaf.tsx @@ -70,7 +70,9 @@ const MemoizedLeaf = React.memo(Leaf, (prev, next) => { next.isLast === prev.isLast && next.renderLeaf === prev.renderLeaf && next.text === prev.text && - Text.matches(next.leaf, prev.leaf) + next.leaf.text === prev.leaf.text && + Text.matches(next.leaf, prev.leaf) && + next.leaf[PLACEHOLDER_SYMBOL] === prev.leaf[PLACEHOLDER_SYMBOL] ) }) diff --git a/packages/slate-react/src/components/text.tsx b/packages/slate-react/src/components/text.tsx index f37a0ff6a..b83283f90 100644 --- a/packages/slate-react/src/components/text.tsx +++ b/packages/slate-react/src/components/text.tsx @@ -10,6 +10,7 @@ import { NODE_TO_ELEMENT, ELEMENT_TO_NODE, } from '../utils/weak-maps' +import { isDecoratorRangeListEqual } from '../utils/range-list' /** * Text. @@ -68,7 +69,8 @@ const MemoizedText = React.memo(Text, (prev, next) => { next.parent === prev.parent && next.isLast === prev.isLast && next.renderLeaf === prev.renderLeaf && - next.text === prev.text + next.text === prev.text && + isDecoratorRangeListEqual(next.decorations, prev.decorations) ) }) diff --git a/packages/slate-react/src/utils/range-list.ts b/packages/slate-react/src/utils/range-list.ts new file mode 100644 index 000000000..71aff0697 --- /dev/null +++ b/packages/slate-react/src/utils/range-list.ts @@ -0,0 +1,43 @@ +import { Range } from 'slate' +import { PLACEHOLDER_SYMBOL } from './weak-maps' + +export const shallowCompare = (obj1: {}, obj2: {}) => + Object.keys(obj1).length === Object.keys(obj2).length && + Object.keys(obj1).every( + key => obj2.hasOwnProperty(key) && obj1[key] === obj2[key] + ) + +/** + * Check if a list of decorator ranges are equal to another. + * + * PERF: this requires the two lists to also have the ranges inside them in the + * same order, but this is an okay constraint for us since decorations are + * kept in order, and the odd case where they aren't is okay to re-render for. + */ + +export const isDecoratorRangeListEqual = ( + list: Range[], + another: Range[] +): boolean => { + if (list.length !== another.length) { + return false + } + + for (let i = 0; i < list.length; i++) { + const range = list[i] + const other = another[i] + + const { anchor: rangeAnchor, focus: rangeFocus, ...rangeOwnProps } = range + const { anchor: otherAnchor, focus: otherFocus, ...otherOwnProps } = other + + if ( + !Range.equals(range, other) || + range[PLACEHOLDER_SYMBOL] !== other[PLACEHOLDER_SYMBOL] || + !shallowCompare(rangeOwnProps, otherOwnProps) + ) { + return false + } + } + + return true +} diff --git a/packages/slate/src/interfaces/text.ts b/packages/slate/src/interfaces/text.ts index c8dd7110e..cdadaef69 100755 --- a/packages/slate/src/interfaces/text.ts +++ b/packages/slate/src/interfaces/text.ts @@ -95,7 +95,7 @@ export const Text: TextInterface = { continue } - if (text[key] !== props[key]) { + if (!text.hasOwnProperty(key) || text[key] !== props[key]) { return false } } diff --git a/packages/slate/test/interfaces/Text/matches/undefined-false.js b/packages/slate/test/interfaces/Text/matches/undefined-false.js new file mode 100644 index 000000000..e08a57af6 --- /dev/null +++ b/packages/slate/test/interfaces/Text/matches/undefined-false.js @@ -0,0 +1,12 @@ +import { Text } from 'slate' + +export const input = { + text: { foo: undefined }, + props: { bar: undefined }, +} + +export const test = ({ text, props }) => { + return Text.matches(text, props) +} + +export const output = false diff --git a/packages/slate/test/interfaces/Text/matches/undefined-true.js b/packages/slate/test/interfaces/Text/matches/undefined-true.js new file mode 100644 index 000000000..a9010c751 --- /dev/null +++ b/packages/slate/test/interfaces/Text/matches/undefined-true.js @@ -0,0 +1,12 @@ +import { Text } from 'slate' + +export const input = { + text: { foo: undefined }, + props: { foo: undefined }, +} + +export const test = ({ text, props }) => { + return Text.matches(text, props) +} + +export const output = true