diff --git a/packages/docusaurus-theme-common/src/contexts/navbarSecondaryMenu/content.tsx b/packages/docusaurus-theme-common/src/contexts/navbarSecondaryMenu/content.tsx index 9c4b9f2ba5b9..6972947a81a9 100644 --- a/packages/docusaurus-theme-common/src/contexts/navbarSecondaryMenu/content.tsx +++ b/packages/docusaurus-theme-common/src/contexts/navbarSecondaryMenu/content.tsx @@ -12,8 +12,7 @@ import React, { type ReactNode, type ComponentType, } from 'react'; -import {ReactContextError} from '../../utils/reactUtils'; -import useShallowMemoObject from '../../hooks/useShallowMemoObject'; +import {ReactContextError, useShallowMemoObject} from '../../utils/reactUtils'; // This context represents a "global layout store". A component (usually a // layout component) can request filling this store through diff --git a/packages/docusaurus-theme-common/src/hooks/useMutationObserver.ts b/packages/docusaurus-theme-common/src/hooks/useMutationObserver.ts index 8ce4e701dcfa..fc11277a02b5 100644 --- a/packages/docusaurus-theme-common/src/hooks/useMutationObserver.ts +++ b/packages/docusaurus-theme-common/src/hooks/useMutationObserver.ts @@ -5,8 +5,7 @@ * LICENSE file in the root directory of this source tree. */ import {useEffect} from 'react'; -import {useDynamicCallback} from '../utils/reactUtils'; -import useShallowMemoObject from './useShallowMemoObject'; +import {useDynamicCallback, useShallowMemoObject} from '../utils/reactUtils'; type Options = MutationObserverInit; diff --git a/packages/docusaurus-theme-common/src/hooks/useShallowMemoObject.ts b/packages/docusaurus-theme-common/src/hooks/useShallowMemoObject.ts deleted file mode 100644 index ca8da1f4a60b..000000000000 --- a/packages/docusaurus-theme-common/src/hooks/useShallowMemoObject.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import {useMemo} from 'react'; - -export default function useShallowMemoObject(obj: O): O { - return useMemo( - () => obj, - // Is this safe? - // eslint-disable-next-line react-hooks/exhaustive-deps - [...Object.keys(obj), ...Object.values(obj)], - ); -} diff --git a/packages/docusaurus-theme-common/src/utils/__tests__/reactUtils.test.ts b/packages/docusaurus-theme-common/src/utils/__tests__/reactUtils.test.ts index 3aaec1e34704..df6e985d01fa 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/reactUtils.test.ts +++ b/packages/docusaurus-theme-common/src/utils/__tests__/reactUtils.test.ts @@ -6,7 +6,7 @@ */ import {renderHook} from '@testing-library/react-hooks'; -import {usePrevious} from '../reactUtils'; +import {usePrevious, useShallowMemoObject} from '../reactUtils'; describe('usePrevious', () => { it('returns the previous value of a variable', () => { @@ -20,3 +20,37 @@ describe('usePrevious', () => { expect(result.current).toBe(2); }); }); + +describe('useShallowMemoObject', () => { + it('can memoize object', () => { + const someObj = {hello: 'world'}; + const someArray = ['hello', 'world']; + + const obj1 = {a: 1, b: '2', someObj, someArray}; + const {result, rerender} = renderHook((val) => useShallowMemoObject(val), { + initialProps: obj1, + }); + expect(result.current).toBe(obj1); + + const obj2 = {a: 1, b: '2', someObj, someArray}; + rerender(obj2); + expect(result.current).toBe(obj1); + + const obj3 = {a: 1, b: '2', someObj, someArray}; + rerender(obj3); + expect(result.current).toBe(obj1); + + // Current implementation is basic and sensitive to order + const obj4 = {b: '2', a: 1, someObj, someArray}; + rerender(obj4); + expect(result.current).toBe(obj4); + + const obj5 = {b: '2', a: 1, someObj, someArray}; + rerender(obj5); + expect(result.current).toBe(obj4); + + const obj6 = {b: '2', a: 1, someObj: {...someObj}, someArray}; + rerender(obj6); + expect(result.current).toBe(obj6); + }); +}); diff --git a/packages/docusaurus-theme-common/src/utils/reactUtils.tsx b/packages/docusaurus-theme-common/src/utils/reactUtils.tsx index 4e0982807121..e03d3c8de609 100644 --- a/packages/docusaurus-theme-common/src/utils/reactUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/reactUtils.tsx @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {useCallback, useEffect, useLayoutEffect, useRef} from 'react'; +import {useCallback, useEffect, useLayoutEffect, useMemo, useRef} from 'react'; import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; /** @@ -74,3 +74,23 @@ export class ReactContextError extends Error { } is called outside the <${providerName}>. ${additionalInfo ?? ''}`; } } + +/** + * Shallow-memoize an object + * + * This means the returned object will be the same as the previous render + * if the attribute names and identities did not change. + * + * This works for simple cases: when attributes are primitives or stable objects + * + * @param obj + */ +export function useShallowMemoObject(obj: O): O { + return useMemo( + () => obj, + // Is this safe? + // TODO make this implementation not order-dependent? + // eslint-disable-next-line react-hooks/exhaustive-deps + [...Object.keys(obj), ...Object.values(obj)], + ); +}