From 532cc41f3b2d55a7f6f297ea9a4b652072e0d311 Mon Sep 17 00:00:00 2001 From: Arttu Olli Date: Sun, 23 Oct 2022 21:00:43 +0300 Subject: [PATCH] feat(useDeepCompareMemo): Implement useDeepCompareMemo (#979) * feat(useDeepCompareMemo): Implement useDeepCompareMemo Implement useDeepCompareMemo by combining useCustomCompareMemo and the isEqual function from react-hookz/deep-equal. Closes #871 Co-authored-by: xobotyi --- README.md | 3 ++ src/index.ts | 2 + src/useDeepCompareEffect/__docs__/story.mdx | 1 + .../__docs__/example.stories.tsx | 27 ++++++++++++ src/useDeepCompareMemo/__docs__/story.mdx | 42 +++++++++++++++++++ src/useDeepCompareMemo/__tests__/dom.ts | 31 ++++++++++++++ src/useDeepCompareMemo/__tests__/ssr.ts | 13 ++++++ src/useDeepCompareMemo/useDeepCompareMemo.ts | 15 +++++++ 8 files changed, 134 insertions(+) create mode 100644 src/useDeepCompareMemo/__docs__/example.stories.tsx create mode 100644 src/useDeepCompareMemo/__docs__/story.mdx create mode 100644 src/useDeepCompareMemo/__tests__/dom.ts create mode 100644 src/useDeepCompareMemo/__tests__/ssr.ts create mode 100644 src/useDeepCompareMemo/useDeepCompareMemo.ts diff --git a/README.md b/README.md index 4779ceba..9c060e63 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,9 @@ Coming from `react-use`? Check out our — Like `useRef`, but it returns immutable ref that contains actual value. - [**`useCustomCompareMemo`**](https://react-hookz.github.io/web/?path=/docs/miscellaneous-useCustomCompareMemo--example) — Like useMemo but uses provided comparator function to validate dependency changes. + - [**`useDeepCompareMemo`**](https://react-hookz.github.io/web/?path=/docs/miscellaneous-useDeepCompareMemo--example) + — Like `useMemo` but uses `@react-hookz/deep-equal` comparator function to validate deep + dependency changes. - [**`useHookableRef`**](https://react-hookz.github.io/web/?path=/docs/miscellaneous-usehookableref--example) — Like `useRef` but it is possible to define get and set handlers. diff --git a/src/index.ts b/src/index.ts index 0f7cdf2b..3cffc7cc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -114,3 +114,5 @@ export { resolveHookState } from './util/resolveHookState'; // Types export * from './types'; + +export { useDeepCompareMemo } from './useDeepCompareMemo/useDeepCompareMemo'; diff --git a/src/useDeepCompareEffect/__docs__/story.mdx b/src/useDeepCompareEffect/__docs__/story.mdx index 4388648f..e59d7cfd 100644 --- a/src/useDeepCompareEffect/__docs__/story.mdx +++ b/src/useDeepCompareEffect/__docs__/story.mdx @@ -11,6 +11,7 @@ changes. - SSR-friendly, meaning that comparator won't be called on the server. - Ability to change underlying effect hook (default to `useEffect`). +- Uses yet fastest deep-comparator - [@react-hookz/deep-equal](https://github.com/react-hookz/deep-equal). #### Example diff --git a/src/useDeepCompareMemo/__docs__/example.stories.tsx b/src/useDeepCompareMemo/__docs__/example.stories.tsx new file mode 100644 index 00000000..8bdb9fbb --- /dev/null +++ b/src/useDeepCompareMemo/__docs__/example.stories.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { useMemo } from 'react'; +import { useRerender } from '../../useRerender/useRerender'; +import { useDeepCompareMemo } from '../useDeepCompareMemo'; + +export const Example: React.FC = () => { + const newOnEveryRender = { value: 'Foo' }; + // eslint-disable-next-line react-hooks/exhaustive-deps + const unstable = useMemo(() => Math.floor(Math.random() * 10), [newOnEveryRender]); + + const stable = useDeepCompareMemo(() => Math.floor(Math.random() * 10), [newOnEveryRender]); + + const rerender = useRerender(); + return ( + <> +
+

When you click this button:

+ +

, you notice, that the useDeepCompareMemo value does not change at all,

+
+

even though its dependencies change on every render.

+
+

useMemo: {unstable}

+

useDeepCompareMemo: {stable}

+ + ); +}; diff --git a/src/useDeepCompareMemo/__docs__/story.mdx b/src/useDeepCompareMemo/__docs__/story.mdx new file mode 100644 index 00000000..9069b854 --- /dev/null +++ b/src/useDeepCompareMemo/__docs__/story.mdx @@ -0,0 +1,42 @@ +import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks' +import { Example } from './example.stories' +import { ImportPath } from '../../__docs__/ImportPath' + + + +# useDeepCompareMemo + +Like `useMemo` but uses `@react-hookz/deep-equal` comparator function to validate deep dependency changes. + +- SSR-friendly, meaning that the comparator won't be called on the server. +- Uses yet fastest deep-comparator - [@react-hookz/deep-equal](https://github.com/react-hookz/deep-equal). + +#### Example + + + + + +## Reference + +```ts +export function useDeepCompareMemo( + factory: () => T, + deps: Deps +): T +``` + +#### Importing + + + +#### Arguments + +- **factory** `() => T` - Function calculating the memoized value. Passed to the underlying `useMemo`. +- **deps** `DependencyList` - List of all reactive values referenced by `factory`. Passed to the `deps` parameter of the underlying `useMemo`. + +#### Return + +Initially returns the result of calling `factory`. This value is memoized and returned on every +render, until the dependencies change, determined by deep comparison, at which point `factory` will be called again and the resulting +value will be memoized. diff --git a/src/useDeepCompareMemo/__tests__/dom.ts b/src/useDeepCompareMemo/__tests__/dom.ts new file mode 100644 index 00000000..9f4bad72 --- /dev/null +++ b/src/useDeepCompareMemo/__tests__/dom.ts @@ -0,0 +1,31 @@ +import { renderHook } from '@testing-library/react-hooks/dom'; +import { useDeepCompareMemo } from '../useDeepCompareMemo'; + +describe('useDeepCompareMemo', () => { + it('should be defined', () => { + expect(useDeepCompareMemo).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useDeepCompareMemo(() => {}, [])); + expect(result.error).toBeUndefined(); + }); + + it('should run only if dependencies change, defined by deep comparison', () => { + const spy = jest.fn(); + const { rerender } = renderHook(({ deps }) => useDeepCompareMemo(spy, deps), { + initialProps: { deps: [{ foo: 'bar' }] }, + }); + + expect(spy).toHaveBeenCalledTimes(1); + + rerender({ deps: [{ foo: 'bar' }] }); + expect(spy).toHaveBeenCalledTimes(1); + + rerender({ deps: [{ foo: 'baz' }] }); + expect(spy).toHaveBeenCalledTimes(2); + + rerender({ deps: [{ foo: 'baz' }] }); + expect(spy).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/useDeepCompareMemo/__tests__/ssr.ts b/src/useDeepCompareMemo/__tests__/ssr.ts new file mode 100644 index 00000000..65ccae3c --- /dev/null +++ b/src/useDeepCompareMemo/__tests__/ssr.ts @@ -0,0 +1,13 @@ +import { renderHook } from '@testing-library/react-hooks/server'; +import { useDeepCompareMemo } from '../..'; + +describe('useDeepCompareMemo', () => { + it('should be defined', () => { + expect(useDeepCompareMemo).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useDeepCompareMemo(() => {}, [])); + expect(result.error).toBeUndefined(); + }); +}); diff --git a/src/useDeepCompareMemo/useDeepCompareMemo.ts b/src/useDeepCompareMemo/useDeepCompareMemo.ts new file mode 100644 index 00000000..026cfc62 --- /dev/null +++ b/src/useDeepCompareMemo/useDeepCompareMemo.ts @@ -0,0 +1,15 @@ +import { DependencyList } from 'react'; +import { isEqual } from '@react-hookz/deep-equal'; +import { useCustomCompareMemo } from '../useCustomCompareMemo/useCustomCompareMemo'; + +/** + * Like useMemo but validates dependency changes using deep equality check instead of reference check. + * + * @param factory Function calculating the value to be memoized. + * @param deps The list of all reactive values referenced inside `factory`. + * @returns Initially returns the result of calling `factory`. On subsequent renders, it will return + * the same value, if dependencies haven't changed, or the result of calling `factory` again, if they have changed. + */ +export function useDeepCompareMemo(factory: () => T, deps: Deps) { + return useCustomCompareMemo(factory, deps, isEqual); +}