diff --git a/CHANGELOG.md b/CHANGELOG.md index d193de00..2c20bf9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# [16.1.0](https://github.com/react-hookz/web/compare/v16.0.1...v16.1.0) (2022-10-23) + + +### Features + +* **useDeepCompareMemo:** Implement useDeepCompareMemo ([#979](https://github.com/react-hookz/web/issues/979)) ([532cc41](https://github.com/react-hookz/web/commit/532cc41f3b2d55a7f6f297ea9a4b652072e0d311)), closes [#871](https://github.com/react-hookz/web/issues/871) + +## [16.0.1](https://github.com/react-hookz/web/compare/v16.0.0...v16.0.1) (2022-10-22) + + +### Bug Fixes + +* **useCustomCompareMemo:** Correctly infer the type of the value returned by the factory function ([#976](https://github.com/react-hookz/web/issues/976)) ([a625c55](https://github.com/react-hookz/web/commit/a625c55bc544dc1bc7544a5ff8811a0a78c568bd)), closes [#975](https://github.com/react-hookz/web/issues/975) + # [16.0.0](https://github.com/react-hookz/web/compare/v15.1.0...v16.0.0) (2022-10-09) diff --git a/README.md b/README.md index 55b06705..9c060e63 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ import { useMountEffect } from '@react-hookz/web/esnext'; ## Migrating from react-use +`@react-hookz/web` was built as a [spiritual successor](https://github.com/streamich/react-use/issues/1974) of `react-use` by one of its former maintainers. + Coming from `react-use`? Check out our [migration guide](https://react-hookz.github.io/web/?path=/docs/migrating-from-react-use--page). @@ -156,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/package.json b/package.json index 53550ab1..897d4ed7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@react-hookz/web", - "version": "16.0.0", + "version": "16.1.0", "description": "React hooks done right, for browser and SSR.", "keywords": [ "react", @@ -107,7 +107,7 @@ "babel-loader": "^8.2.5", "commitizen": "^4.2.5", "commitlint": "^17.1.2", - "concurrently": "^7.4.0", + "concurrently": "^7.5.0", "husky": "^8.0.1", "jest": "^29.2.1", "jest-environment-jsdom": "^29.2.1", diff --git a/src/__docs__/Introduction.story.mdx b/src/__docs__/Introduction.story.mdx index 55b491a9..43b98aee 100644 --- a/src/__docs__/Introduction.story.mdx +++ b/src/__docs__/Introduction.story.mdx @@ -14,7 +14,7 @@ import { Meta } from '@storybook/addon-docs'; [![Types](https://flat.badgen.net/npm/types/@react-hookz/web)](https://www.npmjs.com/package/@react-hookz/web) [![Tree Shaking](https://flat.badgen.net/bundlephobia/tree-shaking/@react-hookz/web)](https://bundlephobia.com/result?p=@react-hookz/web) -× **[DOCS](https://react-hookz.github.io/web/)** × **[DISCORD](https://discord.gg/Fjwphtu65f)** × +× **[GITHUB](https://github.com/react-hookz/web)** × **[DISCORD](https://discord.gg/Fjwphtu65f)** × **[CHANGELOG](https://github.com/react-hookz/web/blob/master/CHANGELOG.md)** × @@ -35,7 +35,7 @@ yarn add @react-hookz/web ``` As hooks was introduced to the world in React 16.8, `@react-hookz/web` requires - you guessed it - -`react` and `react-dom` 16.8+. +`react` and `react-dom` 16.8+. Also, as React does not support IE, `@react-hookz/web` does not do so either. You'll have to transpile your `node-modules` in order to run in IE. @@ -58,3 +58,10 @@ import { useMountEffect } from '@react-hookz/web/esm'; // in case you want all the recent ES features import { useMountEffect } from '@react-hookz/web/esnext'; ``` + +## Migrating from react-use + +`@react-hookz/web` was built as a [spiritual successor](https://github.com/streamich/react-use/issues/1974) of `react-use` by one of its former maintainers. + +Coming from `react-use`? Check out our +[migration guide](https://react-hookz.github.io/web/?path=/docs/migrating-from-react-use--page). 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/useCustomCompareMemo/__docs__/story.mdx b/src/useCustomCompareMemo/__docs__/story.mdx index e9e4bd1d..fdcf3a8b 100644 --- a/src/useCustomCompareMemo/__docs__/story.mdx +++ b/src/useCustomCompareMemo/__docs__/story.mdx @@ -22,11 +22,11 @@ export type DependenciesComparator b: Deps ) => boolean; -function useCustomCompareMemo unknown, Deps extends DependencyList>( - factory: Factory, +function useCustomCompareMemo( + factory: () => T, deps: Deps, comparator: DependenciesComparator -): ReturnType; +): T ``` #### Importing diff --git a/src/useCustomCompareMemo/useCustomCompareMemo.ts b/src/useCustomCompareMemo/useCustomCompareMemo.ts index 6facbf39..63be76ae 100644 --- a/src/useCustomCompareMemo/useCustomCompareMemo.ts +++ b/src/useCustomCompareMemo/useCustomCompareMemo.ts @@ -10,11 +10,11 @@ import type { DependenciesComparator } from '../types'; * @param comparator function to validate dependency changes * @returns useMemo result */ -export const useCustomCompareMemo = unknown, Deps extends DependencyList>( - factory: Factory, +export const useCustomCompareMemo = ( + factory: () => T, deps: Deps, comparator: DependenciesComparator -) => { +): T => { const dependencies = useRef(); if (dependencies.current === undefined || !comparator(dependencies.current, deps)) { @@ -22,5 +22,5 @@ export const useCustomCompareMemo = unknown, Deps extends } // eslint-disable-next-line react-hooks/exhaustive-deps -- missing factory function - return useMemo(factory, dependencies.current); + return useMemo(factory, dependencies.current); }; 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); +} diff --git a/yarn.lock b/yarn.lock index 2d4f9063..1a4f8c1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5732,10 +5732,10 @@ concat-stream@^1.5.0: readable-stream "^2.2.2" typedarray "^0.0.6" -concurrently@^7.4.0: - version "7.4.0" - resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-7.4.0.tgz#bb0e344964bc172673577c420db21e963f2f7368" - integrity sha512-M6AfrueDt/GEna/Vg9BqQ+93yuvzkSKmoTixnwEJkH0LlcGrRC2eCmjeG1tLLHIYfpYJABokqSGyMcXjm96AFA== +concurrently@^7.5.0: + version "7.5.0" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-7.5.0.tgz#4dd432d4634a8251f27ab000c4974e78e3906bd3" + integrity sha512-5E3mwiS+i2JYBzr5BpXkFxOnleZTMsG+WnE/dCG4/P+oiVXrbmrBwJ2ozn4SxwB2EZDrKR568X+puVohxz3/Mg== dependencies: chalk "^4.1.0" date-fns "^2.29.1"