diff --git a/.changeset/dull-eels-hammer.md b/.changeset/dull-eels-hammer.md index a176d5adb18..6b10d691207 100644 --- a/.changeset/dull-eels-hammer.md +++ b/.changeset/dull-eels-hammer.md @@ -2,4 +2,4 @@ 'slate-react': minor --- -Reduce re-renders caused by decorations updates if only the path field is modified. It is possible by adding the `basePath` field to the `Point` type. +If TextComponent decorations keep the same offsets and only paths are changed, prevent re-rendering because only decoration offsets matter when leaves are calculated. diff --git a/packages/slate-react/src/components/element.tsx b/packages/slate-react/src/components/element.tsx index 52b15210315..6d20b03549e 100644 --- a/packages/slate-react/src/components/element.tsx +++ b/packages/slate-react/src/components/element.tsx @@ -12,7 +12,7 @@ import { NODE_TO_INDEX, EDITOR_TO_KEY_TO_ELEMENT, } from '../utils/weak-maps' -import { isDecoratorRangeListEqual } from '../utils/range-list' +import { isElementDecorationsEqual } from '../utils/range-list' import { RenderElementProps, RenderLeafProps, @@ -139,7 +139,7 @@ const MemoizedElement = React.memo(Element, (prev, next) => { prev.element === next.element && prev.renderElement === next.renderElement && prev.renderLeaf === next.renderLeaf && - isDecoratorRangeListEqual(prev.decorations, next.decorations) && + isElementDecorationsEqual(prev.decorations, next.decorations) && (prev.selection === next.selection || (!!prev.selection && !!next.selection && diff --git a/packages/slate-react/src/components/text.tsx b/packages/slate-react/src/components/text.tsx index 1e0a0543797..aec1a369b80 100644 --- a/packages/slate-react/src/components/text.tsx +++ b/packages/slate-react/src/components/text.tsx @@ -2,7 +2,7 @@ import React, { useRef } from 'react' import { Element, Range, Text as SlateText } from 'slate' import { ReactEditor, useSlateStatic } from '..' import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect' -import { isDecoratorRangeListEqual } from '../utils/range-list' +import { isTextDecorationsEqual } from '../utils/range-list' import { EDITOR_TO_KEY_TO_ELEMENT, ELEMENT_TO_NODE, @@ -79,7 +79,7 @@ const MemoizedText = React.memo(Text, (prev, next) => { next.isLast === prev.isLast && next.renderLeaf === prev.renderLeaf && next.text === prev.text && - isDecoratorRangeListEqual(next.decorations, prev.decorations) + isTextDecorationsEqual(next.decorations, prev.decorations) ) }) diff --git a/packages/slate-react/src/custom-types.ts b/packages/slate-react/src/custom-types.ts index 9dcbd0ba2d4..9c91b64c1dc 100644 --- a/packages/slate-react/src/custom-types.ts +++ b/packages/slate-react/src/custom-types.ts @@ -1,4 +1,4 @@ -import { BasePoint, BaseRange, BaseText, Path } from 'slate' +import { BaseRange, BaseText } from 'slate' import { ReactEditor } from './plugin/react-editor' declare module 'slate' { @@ -10,8 +10,5 @@ declare module 'slate' { Range: BaseRange & { placeholder?: string } - Point: BasePoint & { - basePath?: Path - } } } diff --git a/packages/slate-react/src/hooks/use-children.tsx b/packages/slate-react/src/hooks/use-children.tsx index 3901463d630..e9997b94a70 100644 --- a/packages/slate-react/src/hooks/use-children.tsx +++ b/packages/slate-react/src/hooks/use-children.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Editor, Range, Element, Ancestor, Descendant, Point } from 'slate' +import { Editor, Range, Element, Ancestor, Descendant } from 'slate' import ElementComponent from '../components/element' import TextComponent from '../components/text' @@ -52,7 +52,7 @@ const useChildren = (props: { const ds = decorate([n, p]) for (const dec of decorations) { - const d = Range.intersection(toAbsoluteRange(dec), range) + const d = Range.intersection(dec, range) if (d) { ds.push(d) @@ -76,7 +76,7 @@ const useChildren = (props: { } else { children.push( { - const absAnchor = toAbsolutePoint(range.anchor) - const absFocus = toAbsolutePoint(range.focus) - - if (absAnchor === range.anchor && absFocus === absFocus) { - return range - } - - return { - ...range, - anchor: toAbsolutePoint(range.anchor), - focus: toAbsolutePoint(range.focus), - } -} - -const toAbsolutePoint = (point: Point) => { - return point.basePath - ? { - path: [...point.basePath, ...point.path], - offset: point.offset, - } - : point -} - export default useChildren diff --git a/packages/slate-react/src/utils/range-list.ts b/packages/slate-react/src/utils/range-list.ts index 71aff069796..eae4ccf5ed8 100644 --- a/packages/slate-react/src/utils/range-list.ts +++ b/packages/slate-react/src/utils/range-list.ts @@ -7,6 +7,16 @@ export const shallowCompare = (obj1: {}, obj2: {}) => key => obj2.hasOwnProperty(key) && obj1[key] === obj2[key] ) +const isDecorationFlagsEqual = (range: Range, other: Range) => { + const { anchor: rangeAnchor, focus: rangeFocus, ...rangeOwnProps } = range + const { anchor: otherAnchor, focus: otherFocus, ...otherOwnProps } = other + + return ( + range[PLACEHOLDER_SYMBOL] === other[PLACEHOLDER_SYMBOL] && + shallowCompare(rangeOwnProps, otherOwnProps) + ) +} + /** * Check if a list of decorator ranges are equal to another. * @@ -15,7 +25,7 @@ export const shallowCompare = (obj1: {}, obj2: {}) => * kept in order, and the odd case where they aren't is okay to re-render for. */ -export const isDecoratorRangeListEqual = ( +export const isElementDecorationsEqual = ( list: Range[], another: Range[] ): boolean => { @@ -27,13 +37,39 @@ export const isDecoratorRangeListEqual = ( 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) || !isDecorationFlagsEqual(range, other)) { + return false + } + } + + return true +} + +/** + * 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 isTextDecorationsEqual = ( + 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] + // compare only offsets because paths doesn't matter for text if ( - !Range.equals(range, other) || - range[PLACEHOLDER_SYMBOL] !== other[PLACEHOLDER_SYMBOL] || - !shallowCompare(rangeOwnProps, otherOwnProps) + range.anchor.offset !== other.anchor.offset || + range.focus.offset !== other.focus.offset || + !isDecorationFlagsEqual(range, other) ) { return false } diff --git a/site/examples/code-highlighting.tsx b/site/examples/code-highlighting.tsx index 8c596bf0aad..cdaf145d07a 100644 --- a/site/examples/code-highlighting.tsx +++ b/site/examples/code-highlighting.tsx @@ -292,13 +292,11 @@ const ExtractRanges = () => { } const anchor: Point = { - basePath: [index, 0], - path: [], + path: [index, 0], offset: startOffset, } const focus: Point = { - basePath: [index, 0], - path: [], + path: [index, 0], offset: endOffset, } diff --git a/site/examples/custom-types.d.ts b/site/examples/custom-types.d.ts index 7cd2ac7cb5e..42c4f3d7269 100644 --- a/site/examples/custom-types.d.ts +++ b/site/examples/custom-types.d.ts @@ -1,4 +1,4 @@ -import { Descendant, BaseEditor, BaseRange, BasePoint, Range } from 'slate' +import { Descendant, BaseEditor, BaseRange, Range } from 'slate' import { ReactEditor } from 'slate-react' import { HistoryEditor } from 'slate-history' @@ -121,7 +121,6 @@ declare module 'slate' { Editor: CustomEditor Element: CustomElement Text: CustomText | EmptyText - Point: BasePoint & { basePath?: number[] } Range: BaseRange & { [key: string]: unknown }