diff --git a/.changeset/tough-falcons-itch.md b/.changeset/tough-falcons-itch.md new file mode 100644 index 000000000..c6146bea2 --- /dev/null +++ b/.changeset/tough-falcons-itch.md @@ -0,0 +1,5 @@ +--- +'slate-react': minor +--- + +Use equalityFn in useSlateSelector during render as well diff --git a/packages/slate-react/src/hooks/use-slate-selector.tsx b/packages/slate-react/src/hooks/use-slate-selector.tsx index d728523b4..288b40481 100644 --- a/packages/slate-react/src/hooks/use-slate-selector.tsx +++ b/packages/slate-react/src/hooks/use-slate-selector.tsx @@ -57,7 +57,13 @@ export function useSlateSelector( selector !== latestSelector.current || latestSubscriptionCallbackError.current ) { - selectedState = selector(getSlate()) + const selectorResult = selector(getSlate()) + + if (equalityFn(latestSelectedState.current, selectorResult)) { + selectedState = latestSelectedState.current + } else { + selectedState = selectorResult + } } else { selectedState = latestSelectedState.current } diff --git a/packages/slate-react/test/use-slate-selector.test.tsx b/packages/slate-react/test/use-slate-selector.test.tsx new file mode 100644 index 000000000..5cc1769ae --- /dev/null +++ b/packages/slate-react/test/use-slate-selector.test.tsx @@ -0,0 +1,67 @@ +/* eslint-disable no-console */ +import React, { useEffect } from 'react' +import { createEditor, Editor, Text, Transforms } from 'slate' +import { act, render, renderHook } from '@testing-library/react' +import { + Slate, + withReact, + Editable, + ReactEditor, + useSlateSelector, +} from '../src' +import _ from 'lodash' + +describe('useSlateSelector', () => { + test('should use equality function when selector changes', async () => { + const editor = withReact(createEditor()) + const initialValue = [{ type: 'block', children: [{ text: 'test' }] }] + + const callback1 = jest.fn(() => []) + const callback2 = jest.fn(() => []) + + const { result, rerender } = renderHook( + ({ callback }) => useSlateSelector(callback, _.isEqual), + { + initialProps: { + callback: callback1, + }, + wrapper: ({ children }) => ( + + + {children} + + ), + } + ) + + // One call in the render body, and one call in the effect + expect(callback1).toBeCalledTimes(2) + + const firstResult = result.current + + await act(async () => { + Transforms.insertText(editor, '!', { at: { path: [0, 0], offset: 4 } }) + }) + + // The new call is from the effect + expect(callback1).toBeCalledTimes(3) + + // Return values should have referential equality because of the custom equality function + expect(firstResult).toBe(result.current) + + // Callback 2 has not been used yet + expect(callback2).toBeCalledTimes(0) + + // Re-render with new function identity + rerender({ callback: callback2 }) + + // Callback 1 is not called + expect(callback1).toBeCalledTimes(3) + + // Callback 2 is used instead + expect(callback2).toBeCalledTimes(1) + + // Return values should have referential equality because of the custom equality function + expect(firstResult).toBe(result.current) + }) +})