diff --git a/.circleci/config.yml b/.circleci/config.yml index 1dd2e5f6bd..c4d761627e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,17 +3,25 @@ version: 2 refs: container: &container docker: - - image: node:12.12.0 + - image: node:12.14.0 working_directory: ~/repo steps: - &Versions run: name: Versions command: node -v && npm -v && yarn -v + - &CacheRestore + restore_cache: + key: dependency-cache-{{ checksum "yarn.lock" }} - &Install run: name: Install Dependencies command: yarn install --pure-lockfile + - &CacheSave + save_cache: + key: dependency-cache-{{ checksum "yarn.lock" }} + paths: + - ./node_modules - &Build run: name: Build @@ -42,8 +50,9 @@ refs: run: name: Post commit status for Storybook command: | - npx cross-ci :run \ - npx commit-status success Storybook "'\${BUILD_VERSION}'" "'https://$CIRCLE_BUILD_NUM-154950925-gh.circle-artifacts.com/0/root/repo/storybook-static/index.html'" + npx cross-ci :run curl -H "'Authorization: token \${GITHUB_TOKEN}' -H 'Accept: application/vnd.github.v3+json'" \ + "'https://api.github.com/repos/\${PROJECT_OWNER}/\${PROJECT_NAME}/statuses/$CIRCLE_SHA1'" -X POST \ + -d "'{\"state\": \"success\", \"context\": \"Storybook\", \"description\": \"\${BUILD_VERSION}\", \"target_url\": \"https://$CIRCLE_BUILD_NUM-154950925-gh.circle-artifacts.com/0/root/repo/storybook-static/index.html\"}'" jobs: all: @@ -51,7 +60,9 @@ jobs: steps: - checkout - *Versions + - *CacheRestore - *Install + - *CacheSave - *Build - *Build_Storybook - *Test @@ -65,7 +76,9 @@ jobs: steps: - checkout - *Versions + - *CacheRestore - *Install + - *CacheSave - *Build - *Build_Storybook - *Test @@ -87,7 +100,9 @@ jobs: steps: - checkout - *Versions + - *CacheRestore - *Install + - *CacheSave - *Build - *Build_Storybook - *Test diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..bfcd240e6f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,25 @@ +# Description + + + + +## Type of change + + +- [ ] Bug fix _(non-breaking change which fixes an issue)_ +- [ ] New feature _(non-breaking change which adds functionality)_ +- [ ] **Breaking change** _(fix or feature that would cause existing functionality to not work as before)_ + +# Checklist +- [ ] Read the [Contributing Guide](https://github.com/streamich/react-use/blob/master/CONTRIBUTING.md) +- [ ] Perform a code self-review +- [ ] Comment the code, particularly in hard-to-understand areas +- [ ] Add documentation +- [ ] Add hook's story at Storybook +- [ ] Cover changes with tests +- [ ] Ensure the test suite passes (`yarn test`) +- [ ] Provide 100% tests coverage +- [ ] Make sure code lints (`yarn lint`). Fix it with `yarn lint:fix` in case of failure. +- [ ] Make sure types are fine (`yarn lint:types`). + + diff --git a/.storybook/config.js b/.storybook/config.js index 9300b1e920..93dcc97cfc 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -12,7 +12,7 @@ setOptions({ sidebarAnimations: false, }); -const req = require.context('../src/', true, /.*\.(stories|story)\.(js|jsx|ts|tsx)?$/); +const req = require.context('../stories/', true, /\.story\.tsx?$/); const loadStories = () => { req.keys().forEach((filename) => req(filename)); diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js index f3eef02591..5771154606 100644 --- a/.storybook/webpack.config.js +++ b/.storybook/webpack.config.js @@ -1,35 +1,39 @@ const path = require('path'); -const {compilerOptions} = require('../tsconfig.json'); +const { compilerOptions } = require('../tsconfig.json'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); -const SRC_PATH = path.join(__dirname, '../src'); +const basedir = path.join(__dirname, '..'); -module.exports = { - module: { - rules: [ - { - test: /\.md?$/, - loader: "markdown-loader", - }, - { - test: /\.tsx?$/, - loader: 'ts-loader', - include: [ - SRC_PATH, - ], - options: { - transpileOnly: true, // use transpileOnly mode to speed-up compilation - compilerOptions: { - ...compilerOptions, - declaration: false, - }, +module.exports = async ({ config, mode }) => { + config.module.rules.push( + { + test: /\.md?$/, + loader: "markdown-loader", + }, + { + test: /\.tsx?$/, + loader: 'ts-loader', + include: [ + path.join(basedir, 'src'), + path.join(basedir, 'stories'), + ], + options: { + transpileOnly: true, // use transpileOnly mode to speed-up compilation + compilerOptions: { + ...compilerOptions, + declaration: false, }, - } - ] - }, - resolve: { - extensions: ['.ts', '.tsx', '.js', '.jsx'], - enforceExtension: false - }, - plugins: [new ForkTsCheckerWebpackPlugin()], + }, + }, + ); + + config.plugins.push(new ForkTsCheckerWebpackPlugin()); + + config.resolve.extensions = ['.ts', '.tsx', '.js', '.jsx']; + config.resolve.enforceExtension = false; + + // disable the hint about too big bundle + config.performance.hints = false; + + return config; }; diff --git a/CHANGELOG.md b/CHANGELOG.md index 78784e127d..fa8894c99e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,288 @@ +# [13.13.0](https://github.com/streamich/react-use/compare/v13.12.2...v13.13.0) (2019-12-27) + + +### Features + +* add useShallowCompareEffect and useCustomCompareEffect hooks ([ba8803e](https://github.com/streamich/react-use/commit/ba8803eab26d2d48028a4b7120a7354c6d318aea)) + +## [13.12.2](https://github.com/streamich/react-use/compare/v13.12.1...v13.12.2) (2019-12-10) + + +### Bug Fixes + +* **useSet:** "has" method in useSet updated to reference latest set object ([4f1d8c2](https://github.com/streamich/react-use/commit/4f1d8c2cbd773f2a26e2eee4fbad88883ea4b405)) + +## [13.12.1](https://github.com/streamich/react-use/compare/v13.12.0...v13.12.1) (2019-12-09) + + +### Bug Fixes + +* **useMap:** methods with side effects should be stable across renders. ([020b4db](https://github.com/streamich/react-use/commit/020b4dbc6c47ab25243ab8af257dd045e6c1bf6d)) + +# [13.12.0](https://github.com/streamich/react-use/compare/v13.11.0...v13.12.0) (2019-12-09) + + +### Features + +* useScrollbarWidth hook; ([#825](https://github.com/streamich/react-use/issues/825)) ([125c7e9](https://github.com/streamich/react-use/commit/125c7e96a188405aea36e94ed1bb3d984232b2f6)) + +# [13.11.0](https://github.com/streamich/react-use/compare/v13.10.2...v13.11.0) (2019-12-08) + + +### Features + +* 🎸 add useUnmountPromise hook ([01421bc](https://github.com/streamich/react-use/commit/01421bc634b941044e95c611f37eb87339486241)) + +## [13.10.2](https://github.com/streamich/react-use/compare/v13.10.1...v13.10.2) (2019-12-05) + + +### Bug Fixes + +* useUpdate hitting maxInt, failing to trigger rerender ([93e7291](https://github.com/streamich/react-use/commit/93e72910abf2dafe5bdff625a21f633afd6e52c5)) + +## [13.10.1](https://github.com/streamich/react-use/compare/v13.10.0...v13.10.1) (2019-12-03) + + +### Bug Fixes + +* "get" method in useMap updated to reference latest map object ([044d267](https://github.com/streamich/react-use/commit/044d2677aa474d19da776444b78bd3d2594c6ae5)) + +# [13.10.0](https://github.com/streamich/react-use/compare/v13.9.0...v13.10.0) (2019-11-28) + + +### Features + +* useStateHistory ([#709](https://github.com/streamich/react-use/issues/709)) ([0a66359](https://github.com/streamich/react-use/commit/0a6635914319e9ef7a2902189a3c2dea90a2bf7f)) + +# [13.9.0](https://github.com/streamich/react-use/compare/v13.8.2...v13.9.0) (2019-11-23) + + +### Features + +* add useFirstMountState & useRendersCount hooks ([#769](https://github.com/streamich/react-use/issues/769)) ([30abe2b](https://github.com/streamich/react-use/commit/30abe2b22e3cb7a3e4c6dedd2466d74ce660911d)) + +## [13.8.2](https://github.com/streamich/react-use/compare/v13.8.1...v13.8.2) (2019-11-22) + + +### Bug Fixes + +* **#792:** make useUnmount invoke the current callback version instead of very first ([75284c6](https://github.com/streamich/react-use/commit/75284c62c8e4a68dfeb41a8d98a1e636e9ef531a)), closes [#792](https://github.com/streamich/react-use/issues/792) + +## [13.8.1](https://github.com/streamich/react-use/compare/v13.8.0...v13.8.1) (2019-11-21) + + +### Bug Fixes + +* useAsyncFn does not discard old promises and might produce races ([022fa0b](https://github.com/streamich/react-use/commit/022fa0b7b77d582a10c6ca61a3dcd901770011c8)) + +# [13.8.0](https://github.com/streamich/react-use/compare/v13.7.0...v13.8.0) (2019-11-14) + + +### Features + +* **useStateValidator:** Refactor method and improve typings; ([436c210](https://github.com/streamich/react-use/commit/436c210f7b577c6958e47df3a244907b07a4db9f)) + +# [13.7.0](https://github.com/streamich/react-use/compare/v13.6.3...v13.7.0) (2019-11-14) + + +### Features + +* Add useSet hook ([095b4de](https://github.com/streamich/react-use/commit/095b4de2321b8bf3431e3f66139629b0495f1ac9)) + +## [13.6.3](https://github.com/streamich/react-use/compare/v13.6.2...v13.6.3) (2019-11-12) + + +### Bug Fixes + +* remove any types in useThrottleFn ([bb5baea](https://github.com/streamich/react-use/commit/bb5baea30cf59721098ca9e3185105bf1b82218b)) + +## [13.6.2](https://github.com/streamich/react-use/compare/v13.6.1...v13.6.2) (2019-11-11) + + +### Bug Fixes + +* restrict useThrottleFn types ([61a83d1](https://github.com/streamich/react-use/commit/61a83d124d35d5606b6c0700faf1361fd3170ca4)) + +## [13.6.1](https://github.com/streamich/react-use/compare/v13.6.0...v13.6.1) (2019-11-10) + + +### Bug Fixes + +* 🐛 check window.Event constructor exists in useLocation ([ad09431](https://github.com/streamich/react-use/commit/ad094311454c48873ba7143654a29b8a0c54459d)) + +# [13.6.0](https://github.com/streamich/react-use/compare/v13.5.0...v13.6.0) (2019-11-10) + + +### Features + +* **useCounter:** `reset` to the newest initialValue ([#667](https://github.com/streamich/react-use/issues/667)) ([e653383](https://github.com/streamich/react-use/commit/e65338372adfccd4800496b377f63bcdf6646788)) + +# [13.5.0](https://github.com/streamich/react-use/compare/v13.4.0...v13.5.0) (2019-11-08) + + +### Bug Fixes + +* **resolveHookState:** by accident removed needed type. ([59aa41c](https://github.com/streamich/react-use/commit/59aa41cff435f5adf7f38d361649761b8dd69794)) + + +### Features + +* **useList:** reimplemented useList hook; ([1840b57](https://github.com/streamich/react-use/commit/1840b577e2a3d321b8dbb44d5ae443e84d4d9e20)) + +# [13.4.0](https://github.com/streamich/react-use/compare/v13.3.0...v13.4.0) (2019-11-08) + + +### Features + +* **useStateList:** implemented `currentIndex`, `setState`, `setStateAt` methods as requested in [#634](https://github.com/streamich/react-use/issues/634); ([43cb6aa](https://github.com/streamich/react-use/commit/43cb6aa612ae869e24f67acf6f0a1712a65f128b)) + +# [13.3.0](https://github.com/streamich/react-use/compare/v13.2.2...v13.3.0) (2019-11-06) + + +### Features + +* useDebounce add cancel ([693aec8](https://github.com/streamich/react-use/commit/693aec8307c378697c88c635a401832f24d3531a)) +* useDebounce add cancel ([26cab31](https://github.com/streamich/react-use/commit/26cab31f10a995ec45d6c1e2a2f724d9994d801f)) + +## [13.2.2](https://github.com/streamich/react-use/compare/v13.2.1...v13.2.2) (2019-11-06) + + +### Bug Fixes + +* **#749:** now should work with SSR ([c12976c](https://github.com/streamich/react-use/commit/c12976cad26577a4be3ac65133268f41bbdc82da)), closes [#749](https://github.com/streamich/react-use/issues/749) + +## [13.2.1](https://github.com/streamich/react-use/compare/v13.2.0...v13.2.1) (2019-11-04) + + +### Bug Fixes + +* **yarn.lock:** re-create the yarn.lock file with nailed versions in devDeps ([f094a3a](https://github.com/streamich/react-use/commit/f094a3ae833f406137b9d5355843a6615af20164)) + +# [13.2.0](https://github.com/streamich/react-use/compare/v13.1.0...v13.2.0) (2019-11-04) + + +### Features + +* re-create yarn.lock ([d48e03e](https://github.com/streamich/react-use/commit/d48e03e9ee38555ff29ca46fb6e75c13e9c23aba)) +* re-create yarn.lock ([ccdffe0](https://github.com/streamich/react-use/commit/ccdffe027fba15bdca1f35dc375a0c32739aee6f)) +* **usePreviousDistinct:** add tests for undefined value behaviour; ([cb373f9](https://github.com/streamich/react-use/commit/cb373f951fb3f34b9e54793687de14000a2dc08e)) +* **usePreviousDistinct:** improve types; ([30f53e8](https://github.com/streamich/react-use/commit/30f53e8c5d7e8b27bf3f273ebfcacabf30146ba3)) +* **usePreviousDistinct:** now predicate not called on initial render; ([fbe9b13](https://github.com/streamich/react-use/commit/fbe9b1303c0433d5608ca5b507d9c76711b5cb68)) +* **useStateList:** rework useStateList to make it work properly. ([242c274](https://github.com/streamich/react-use/commit/242c274dd49779fa80f8b9e451c699205279339e)) +* **useTitle:** reworked hook to make it synchronous without useUpdate; ([a133267](https://github.com/streamich/react-use/commit/a13326779ffd6885ac531240b984a77bcad3bee6)) +* **useWindowSize:** A bit changed lyfecycle and added types; ([03bdecf](https://github.com/streamich/react-use/commit/03bdecf7ac6aa0ad863a1efd71c056aa41df62b7)) + +# [13.1.0](https://github.com/streamich/react-use/compare/v13.0.1...v13.1.0) (2019-11-01) + + +### Features + +* pull request template for features ([1dc21f3](https://github.com/streamich/react-use/commit/1dc21f3)) + +## [13.0.1](https://github.com/streamich/react-use/compare/v13.0.0...v13.0.1) (2019-11-01) + + +### Bug Fixes + +* **useLockBodyScroll:** infer overflow type directly from declaration. ([26baf47](https://github.com/streamich/react-use/commit/26baf47)) + +# [13.0.0](https://github.com/streamich/react-use/compare/v12.13.0...v13.0.0) (2019-11-01) + + +* Merge pull request #711 from streamich/remove-react-wait ([8d40f18](https://github.com/streamich/react-use/commit/8d40f18)), closes [#711](https://github.com/streamich/react-use/issues/711) + + +### Features + +* remove useRefMounted hook ([ad74d3d](https://github.com/streamich/react-use/commit/ad74d3d)) +* **useRefMounted:** remove obsolete hook; ([dc364c8](https://github.com/streamich/react-use/commit/dc364c8)) +* **useWait:** removed from package due to it is simple reexport of other package; ([d7c38bd](https://github.com/streamich/react-use/commit/d7c38bd)) + + +### BREAKING CHANGES + +* useWait hook has been removed from react-use +* deprecated useRefMounted hook is now removed, use useMountedState hook instead + +# [12.13.0](https://github.com/streamich/react-use/compare/v12.12.0...v12.13.0) (2019-10-31) + + +### Features + +* **useCounter:** reworked with use of new resolveHookState function plus improved memory usage; ([befcf84](https://github.com/streamich/react-use/commit/befcf84)) +* **useGetSet:** reworked with use of new resolveHookState function plus improved memory usage; ([9b5d0f2](https://github.com/streamich/react-use/commit/9b5d0f2)) +* react-like state resolver to use it in stateful hooks; ([9fd02eb](https://github.com/streamich/react-use/commit/9fd02eb)) + +# [12.12.0](https://github.com/streamich/react-use/compare/v12.11.0...v12.12.0) (2019-10-31) + + +### Features + +* add typings for createReducer ([f1cf036](https://github.com/streamich/react-use/commit/f1cf036)) + +# [12.11.0](https://github.com/streamich/react-use/compare/v12.10.0...v12.11.0) (2019-10-31) + + +### Features + +* **useWait:** add deprecation messages to readme and export; ([d338245](https://github.com/streamich/react-use/commit/d338245)) + +# [12.10.0](https://github.com/streamich/react-use/compare/v12.9.1...v12.10.0) (2019-10-30) + + +### Features + +* **useUpdate:** improve memory usage - now single function instance to increment all counters; ([0f02fd0](https://github.com/streamich/react-use/commit/0f02fd0)) + +## [12.9.1](https://github.com/streamich/react-use/compare/v12.9.0...v12.9.1) (2019-10-28) + + +### Bug Fixes + +* useSize avoid crash in Safari 11 ([da0e66b](https://github.com/streamich/react-use/commit/da0e66b)) + +# [12.9.0](https://github.com/streamich/react-use/compare/v12.8.0...v12.9.0) (2019-10-26) + + +### Features + +* add createBreakpoint ([79ba4ef](https://github.com/streamich/react-use/commit/79ba4ef)) + +# [12.8.0](https://github.com/streamich/react-use/compare/v12.7.2...v12.8.0) (2019-10-25) + + +### Features + +* add ensuredForwardRef and useEnsuredForwardedRef ([1bfe063](https://github.com/streamich/react-use/commit/1bfe063)) + +## [12.7.2](https://github.com/streamich/react-use/compare/v12.7.1...v12.7.2) (2019-10-23) + + +### Bug Fixes + +* 🐛 bump set-harmonic-interval package version ([f7c709a](https://github.com/streamich/react-use/commit/f7c709a)) + +## [12.7.1](https://github.com/streamich/react-use/compare/v12.7.0...v12.7.1) (2019-10-17) + + +### Bug Fixes + +* example in the docs; ([7f54cad](https://github.com/streamich/react-use/commit/7f54cad)) +* rename story's mediator and add `g` flag to it's regex; ([652b318](https://github.com/streamich/react-use/commit/652b318)) + +# [12.7.0](https://github.com/streamich/react-use/compare/v12.6.0...v12.7.0) (2019-10-17) + + +### Bug Fixes + +* error throw tests; ([056875b](https://github.com/streamich/react-use/commit/056875b)) +* useMultiStateValidator readme description; ([8c7f7f5](https://github.com/streamich/react-use/commit/8c7f7f5)) + + +### Features + +* useMultiStateValidator ([ae26988](https://github.com/streamich/react-use/commit/ae26988)) + # [12.6.0](https://github.com/streamich/react-use/compare/v12.5.0...v12.6.0) (2019-10-16) diff --git a/README.md b/README.md index 712a092db4..64858f9f44 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,6 @@ demos -
Collection of essential React Hooks. Port of libreact. @@ -66,6 +65,8 @@ - [`useWindowScroll`](./docs/useWindowScroll.md) — tracks `Window` scroll position. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usewindowscroll--docs) - [`useWindowSize`](./docs/useWindowSize.md) — tracks `Window` dimensions. [![][img-demo]](https://codesandbox.io/s/m7ln22668) - [`useMeasure`](./docs/useMeasure.md) — tracks an HTML element's dimensions using the Resize Observer API.[![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usemeasure--demo) + - [`createBreakpoint`](./docs/createBreakpoint.md) — tracks `innerWidth` + - [`useScrollbarWidth`](./docs/useScrollbarWidth.md) — detects browser's native scrollbars width. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usescrollbarwidth--demo)

- [**UI**](./docs/UI.md) @@ -76,8 +77,8 @@ - [`useFullscreen`](./docs/useFullscreen.md) — display an element or video full-screen. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/ui-usefullscreen--demo) - [`useSlider`](./docs/useSlider.md) — provides slide behavior over any HTML element. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/ui-useslider--demo) - [`useSpeech`](./docs/useSpeech.md) — synthesizes speech from a text string. [![][img-demo]](https://codesandbox.io/s/n090mqz69m) + - [`useVibrate`](./docs/useVibrate.md) — provide physical feedback using the [Vibration API](https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API). [![][img-demo]](https://streamich.github.io/react-use/?path=/story/ui-usevibrate--demo) - [`useVideo`](./docs/useVideo.md) — plays video, tracks its state, and exposes playback controls. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/ui-usevideo--demo) - - [`useWait`](./docs/useWait.md) — complex waiting management for UIs.

- [**Animations**](./docs/Animations.md) @@ -91,9 +92,7 @@

- [**Side-effects**](./docs/Side-effects.md) - - [`useAsync`](./docs/useAsync.md) — resolves an `async` function. - - [`useAsyncFn`](./docs/useAsyncFn.md) — state management for an async function - - [`useAsyncRetry`](./docs/useAsyncRetry.md) — `useAsync` with `retry()` method. + - [`useAsync`](./docs/useAsync.md), [`useAsyncFn`](./docs/useAsyncFn.md), and [`useAsyncRetry`](./docs/useAsyncRetry.md) — resolves an `async` function. - [`useBeforeUnload`](./docs/useBeforeUnload.md) — shows browser alert when user try to reload or close the page. - [`useCopyToClipboard`](./docs/useCopyToClipboard.md) — copies text to clipboard. - [`useDebounce`](./docs/useDebounce.md) — debounces a function. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/side-effects-usedebounce--demo) @@ -111,14 +110,14 @@ - [`useEffectOnce`](./docs/useEffectOnce.md) — a modified [`useEffect`](https://reactjs.org/docs/hooks-reference.html#useeffect) hook that only runs once. - [`useEvent`](./docs/useEvent.md) — subscribe to events. - [`useLifecycles`](./docs/useLifecycles.md) — calls `mount` and `unmount` callbacks. - - [`useMountedState`](./docs/useMountedState.md) and [`useRefMounted`](./docs/useRefMounted.md) — track if component is mounted. + - [`useMountedState`](./docs/useMountedState.md) and [`useUnmountPromise`](./docs/useUnmountPromise.md) — track if component is mounted. - [`usePromise`](./docs/usePromise.md) — resolves promise only while component is mounted. - [`useLogger`](./docs/useLogger.md) — logs in console as component goes through life-cycles. - [`useMount`](./docs/useMount.md) — calls `mount` callbacks. - [`useUnmount`](./docs/useUnmount.md) — calls `unmount` callbacks. - [`useUpdateEffect`](./docs/useUpdateEffect.md) — run an `effect` only on updates. - [`useIsomorphicLayoutEffect`](./docs/useIsomorphicLayoutEffect.md) — `useLayoutEffect` that does not show warning when server-side rendering. - - [`useDeepCompareEffect`](./docs/useDeepCompareEffect.md) — run an `effect` depending on deep comparison of its dependencies + - [`useDeepCompareEffect`](./docs/useDeepCompareEffect.md), [`useShallowCompareEffect`](./docs/useShallowCompareEffect.md), and [`useCustomCompareEffect`](./docs/useCustomCompareEffect.md) — run an `effect` depending on deep comparison of its dependencies

- [**State**](./docs/State.md) @@ -128,15 +127,28 @@ - [`useGetSet`](./docs/useGetSet.md) — returns state getter `get()` instead of raw state. - [`useGetSetState`](./docs/useGetSetState.md) — as if [`useGetSet`](./docs/useGetSet.md) and [`useSetState`](./docs/useSetState.md) had a baby. - [`usePrevious`](./docs/usePrevious.md) — returns the previous state or props. [![][img-demo]](https://codesandbox.io/s/fervent-galileo-krgx6) + - [`usePreviousDistinct`](./docs/usePreviousDistinct.md) — like `usePrevious` but with a predicate to determine if `previous` should update. - [`useObservable`](./docs/useObservable.md) — tracks latest value of an `Observable`. - [`useRafState`](./docs/useRafState.md) — creates `setState` method which only updates after `requestAnimationFrame`. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-userafstate--demo) - [`useSetState`](./docs/useSetState.md) — creates `setState` method which works like `this.setState`. [![][img-demo]](https://codesandbox.io/s/n75zqn1xp0) - [`useStateList`](./docs/useStateList.md) — circularly iterates over an array. [![][img-demo]](https://codesandbox.io/s/bold-dewdney-pjzkd) - [`useToggle` and `useBoolean`](./docs/useToggle.md) — tracks state of a boolean. [![][img-demo]](https://codesandbox.io/s/focused-sammet-brw2d) - [`useCounter` and `useNumber`](./docs/useCounter.md) — tracks state of a number. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usecounter--demo) - - [`useList`](./docs/useList.md) and [`useUpsert`](./docs/useUpsert.md) — tracks state of an array. [![][img-demo]](https://codesandbox.io/s/wonderful-mahavira-1sm0w) + - [`useList`](./docs/useList.md) ~and [`useUpsert`](./docs/useUpsert.md)~ — tracks state of an array. [![][img-demo]](https://codesandbox.io/s/wonderful-mahavira-1sm0w) - [`useMap`](./docs/useMap.md) — tracks state of an object. [![][img-demo]](https://codesandbox.io/s/quirky-dewdney-gi161) + - [`useSet`](./docs/useSet.md) — tracks state of a Set. [![][img-demo]](https://codesandbox.io/s/bold-shtern-6jlgw) + - [`useQueue`](./docs/useQueue.md) — implements simple queue. - [`useStateValidator`](./docs/useStateValidator.md) — tracks state of an object. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usestatevalidator--demo) + - [`useStateWithHistory`](./docs/useStateWithHistory.md) — stores previous state values and provides handles to travel through them. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usestatewithhistory--demo) + - [`useMultiStateValidator`](./docs/useMultiStateValidator.md) — alike the `useStateValidator`, but tracks multiple states at a time. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usemultistatevalidator--demo) + - [`useMediatedState`](./docs/useMediatedState.md) — like the regular `useState` but with mediation by custom function. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usemediatedstate--demo) + - [`useFirstMountState`](./docs/useFirstMountState.md) — check if current render is first. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usefirstmountstate--demo) + - [`useRendersCount`](./docs/useRendersCount.md) — count component renders. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-userenderscount--demo) +
+
+- [**Miscellaneous**]() + - [`useEnsuredForwardedRef`](./docs/useEnsuredForwardedRef.md) and [`ensuredForwardRef`](./docs/useEnsuredForwardedRef.md) — use a React.forwardedRef safely. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-useensuredforwardedref--demo) +

@@ -171,28 +183,6 @@ -
-
- -
-

Backers

-
- -
-
- - - - - - - - - - - - -


diff --git a/docs/createBreakpoint.md b/docs/createBreakpoint.md new file mode 100644 index 0000000000..a07cb68b15 --- /dev/null +++ b/docs/createBreakpoint.md @@ -0,0 +1,43 @@ +# `createBreakpoint` + +## Usage + +### use default breakpoint + +laptopL: 1440, laptop: 1024, tablet: 768 + +```jsx +import React from "react"; +import { createBreakpoint } from "react-use"; + +const useBreakpoint = createBreakpoint(); + +const Demo = () => { + const breakpoint = useBreakpoint(); + + if (breakpoint === "laptopL") return
This is very big Laptop
; + else if (breakpoint == "laptop") return
This is Laptop
; + else if (breakpoint == "tablet") return
This is Tablet
; + else return
Too small!
; +}; +``` + +### use custom breakpoint + +XL: 1280, L: 768, S: 350 + +```jsx +import React from "react"; +import { createBreakpoint } from "react-use"; + +const useBreakpoint = createBreakpoint({ XL: 1280, L: 768, S: 350 }); + +const Demo = () => { + const breakpoint = useBreakpoint(); + + if (breakpoint === "XL") return
XL
; + else if (breakpoint == "L") return
LoL
; + else if (breakpoint == "S") return
Sexyy
; + else return
Wth
; +}; +``` diff --git a/docs/useCustomCompareEffect.md b/docs/useCustomCompareEffect.md new file mode 100644 index 0000000000..9c90247ec6 --- /dev/null +++ b/docs/useCustomCompareEffect.md @@ -0,0 +1,31 @@ +# `useCustomCompareEffect` + +A modified useEffect hook that accepts a comparator which is used for comparison on dependencies instead of reference equality. + +## Usage + +```jsx +import {useCounter, useDeepCompareEffect} from 'react-use'; +import isEqual from 'lodash/isEqual'; + +const Demo = () => { + const [count, {inc: inc}] = useCounter(0); + const options = { step: 2 }; + + useCustomCompareEffect(() => { + inc(options.step) + }, [options], (prevDeps, nextDeps) => isEqual(prevDeps, nextDeps)); + + return ( +
+

useCustomCompareEffect with deep comparison: {count}

+
+ ); +}; +``` + +## Reference + +```ts +useCustomCompareEffect(effect: () => void | (() => void | undefined), deps: any[], depsEqual: (prevDeps: any[], nextDeps: any[]) => boolean); +``` diff --git a/docs/useDebounce.md b/docs/useDebounce.md index 20a25b27e6..151e9a2af8 100644 --- a/docs/useDebounce.md +++ b/docs/useDebounce.md @@ -7,15 +7,12 @@ The third argument is the array of values that the debounce depends on, in the s ## Usage ```jsx -import React, { useState } from 'react'; -import { useDebounce } from 'react-use'; - const Demo = () => { const [state, setState] = React.useState('Typing stopped'); const [val, setVal] = React.useState(''); const [debouncedValue, setDebouncedValue] = React.useState(''); - useDebounce( + const [, cancel] = useDebounce( () => { setState('Typing stopped'); setDebouncedValue(val); @@ -36,7 +33,10 @@ const Demo = () => { }} />
{state}
-
Debounced value: {debouncedValue}
+
+ Debounced value: {debouncedValue} + +
); }; @@ -45,5 +45,17 @@ const Demo = () => { ## Reference ```ts -useDebounce(fn, ms: number, args: any[]); +const [ + isReady: () => boolean | null, + cancel: () => void, +] = useDebounce(fn: Function, ms: number, deps: DependencyList = []); ``` + +- **`fn`**_`: Function`_ - function that will be called; +- **`ms`**_`: number`_ - delay in milliseconds; +- **`deps`**_`: DependencyList`_ - array of values that the debounce depends on, in the same manner as useEffect; +- **`isReady`**_`: ()=>boolean|null`_ - function returning current debounce state: + - `false` - pending + - `true` - called + - `null` - cancelled +- **`cancel`**_`: ()=>void`_ - cancel the debounce diff --git a/docs/useEnsuredForwardedRef.md b/docs/useEnsuredForwardedRef.md new file mode 100644 index 0000000000..b094a284bc --- /dev/null +++ b/docs/useEnsuredForwardedRef.md @@ -0,0 +1,63 @@ +# `useEnsuredForwardedRef` + +React hook to use a ForwardedRef safely. + +In some scenarios, you may need to use a _ref_ from inside and outside a component. If that's the case, you should use `React.forwardRef` to pass it through the child component. This is useful when you only want to forward that _ref_ and expose an internal `HTMLelement` to a parent component, for example. However, if you need to manipulate that reference inside a child's lifecycle hook... things get complicated, since you can't always ensure that the _ref_ is being sent by the parent component and if it is not, you will get `undefined` instead of a valid _ref_. + +This hook is useful in this specific case, it will __ensure__ that you get a valid reference on the other side. + +## Usage + +```jsx +import {ensuredForwardRef} from 'react-use'; + +const Demo = () => { + return ( + + ); +}; + +const Child = ensuredForwardRef((props, ref) => { + useEffect(() => { + console.log(ref.current.getBoundingClientRect()) + }, []) + + return ( +
+ ); +}); +``` + +## Alternative usage + +```jsx +import {useEnsuredForwardedRef} from 'react-use'; + +const Demo = () => { + return ( + + ); +}; + +const Child = React.forwardRef((props, ref) => { + // Here `ref` is undefined + const ensuredForwardRef = useEnsuredForwardedRef(ref); + // ensuredForwardRef will always be a valid reference. + + useEffect(() => { + console.log(ensuredForwardRef.current.getBoundingClientRect()) + }, []) + + return ( +
+ ); +}); +``` + +## Reference + +```ts +ensuredForwardRef(Component: RefForwardingComponent): ForwardRefExoticComponent & RefAttributes>; + +useEnsuredForwardedRef(ref: React.MutableRefObject): React.MutableRefObject; +``` diff --git a/docs/useFirstMountState.md b/docs/useFirstMountState.md new file mode 100644 index 0000000000..8d2a737389 --- /dev/null +++ b/docs/useFirstMountState.md @@ -0,0 +1,29 @@ +# `useFirstMountState` + +Returns `true` if component is just mounted (on first render) and `false` otherwise. + +## Usage + +```typescript jsx +import * as React from 'react'; +import { useFirstMountState } from 'react-use'; + +const Demo = () => { + const isFirstMount = useFirstMountState(); + const update = useUpdate(); + + return ( +
+ This component is just mounted: {isFirstMount ? 'YES' : 'NO'} +
+ +
+ ); +}; +``` + +## Reference + +```typescript +const isFirstMount: boolean = useFirstMountState(); +``` diff --git a/docs/useList.md b/docs/useList.md index 2e33e188a5..caffe42f6b 100644 --- a/docs/useList.md +++ b/docs/useList.md @@ -1,6 +1,11 @@ # `useList` -React state hook that tracks a value of an array. +Tracks an array and provides methods to modify it. +To cause component re-render you have to use these methods instead of direct interaction with array - it won't cause re-render. + +We can ensure that actions object and actions itself will not mutate or change between renders, so there is no need to add it to useEffect dependencies and safe to pass them down to children. + +**Note:** `remove` action is deprecated and actually is a copy of `removeAt` action. Within closest updates it will gain different functionality. ## Usage @@ -8,7 +13,7 @@ React state hook that tracks a value of an array. import {useList} from 'react-use'; const Demo = () => { - const [list, { clear, filter, push, remove, set, sort, updateAt, reset }] = useList(); + const [list, { set, push, updateAt, insertAt, update, updateFirst, upsert, sort, filter, removeAt, clear, reset }] = useList([1, 2, 3, 4, 5]); return (
@@ -27,6 +32,42 @@ const Demo = () => { }; ``` +## Reference +```ts +import {useList} from "react-use"; + +const [list, { + set, + push, + updateAt, + insertAt, + update, + updateFirst, + upsert, + sort, + filter, + removeAt, + remove, + clear, + reset +}] = useList(array: any[] | ()=> any[]); +``` + +- **`list`**_`: T{}`_ — current list; +- **`set`**_`: (list: T[]) => void;`_ — Set new list instead old one; +- **`push`**_`: (...items: T[]) => void;`_ — Add item(s) at the end of list; +- **`updateAt`**_`: (index: number, item: T) => void;`_ — Replace item at given position. If item at given position not exists it will be set; +- **`insertAt`**_`: (index: number, item: T) => void;`_ — Insert item at given position, all items to the right will be shifted; +- **`update`**_`: (predicate: (a: T, b: T) => boolean, newItem: T) => void;`_ — Replace all items that matches predicate with given one; +- **`updateFirst`**_`: (predicate: (a: T, b: T) => boolean, newItem: T) => void;`_ — Replace first item matching predicate with given one; +- **`upsert`**_`: (predicate: (a: T, b: T) => boolean, newItem: T) => void;`_ — Like `updateFirst` but in case of predicate miss - pushes item to the list; +- **`sort`**_`: (compareFn?: (a: T, b: T) => number) => void;`_ — Sort list with given sorting function; +- **`filter`**_`: (callbackFn: (value: T, index?: number, array?: T[]) => boolean, thisArg?: any) => void;`_ — Same as native Array's method; +- **`removeAt`**_`: (index: number) => void;`_ — Removes item at given position. All items to the right from removed will be shifted; +- **`remove`**_`: (index: number) => void;`_ — _**DEPRECATED:**_ Use removeAt method instead; +- **`clear`**_`: () => void;`_ — Make the list empty; +- **`reset`**_`: () => void;`_ — Reset list to initial value; + ## Related hooks - [useUpsert](./useUpsert.md) diff --git a/docs/useMeasure.md b/docs/useMeasure.md index 0f574fbcbe..d8e87a0cad 100644 --- a/docs/useMeasure.md +++ b/docs/useMeasure.md @@ -12,7 +12,7 @@ const Demo = () => { return (
-
x: {x}
+
x: {x}
y: {y}
width: {width}
height: {height}
diff --git a/docs/useMediatedState.md b/docs/useMediatedState.md new file mode 100644 index 0000000000..0779a2e97d --- /dev/null +++ b/docs/useMediatedState.md @@ -0,0 +1,39 @@ +# `useMediatedState` + +A lot like the standard `useState`, but with mediation process. + +## Usage +```ts +import * as React from 'react'; +import { useMediatedState } from '../useMediatedState'; + +const inputMediator = s => s.replace(/[\s]+/g, ' '); +const Demo = () => { + const [state, setState] = useMediatedState(inputMediator, ''); + + return ( +
+
You will not be able to enter more than one space
+ ) => { + setState(ev.target.value); + }} + /> +
+ ); +}; +``` + +## Reference +```ts +const [state, setState] = useMediatedState( + mediator: StateMediator, + initialState?: S +); +``` + +> Initial state will be set as-is. + +In case mediator expects 2 arguments it will receive the `setState` function as second argument, it is useful for async mediators. +>This hook will not cancel previous mediation when new one been invoked, you have to handle it yourself._ diff --git a/docs/useMount.md b/docs/useMount.md index 95b53ae8b9..7b357c55ff 100644 --- a/docs/useMount.md +++ b/docs/useMount.md @@ -8,7 +8,7 @@ React lifecycle hook that calls a function after the component is mounted. Use ` import {useMount} from 'react-use'; const Demo = () => { - useMount(() => console.log('MOUNTED')); + useMount(() => alert('MOUNTED')); return null; }; ``` diff --git a/docs/useMultiStateValidator.md b/docs/useMultiStateValidator.md new file mode 100644 index 0000000000..42de5b1e88 --- /dev/null +++ b/docs/useMultiStateValidator.md @@ -0,0 +1,55 @@ +# `useMultiStateValidator` + +Each time any of given states changes - validator function is invoked. + +## Usage +```ts +import * as React from 'react'; +import { useMultiStateValidator } from 'react-use'; + +const DemoStateValidator = (s: number[]) => [s.every((num: number) => !(num % 2))] as [boolean]; +const Demo = () => { + const [state1, setState1] = React.useState(1); + const [state2, setState2] = React.useState(1); + const [state3, setState3] = React.useState(1); + const [[isValid]] = useMultiStateValidator([state1, state2, state3], DemoStateValidator); + + return ( +
+
Below fields will be valid if all of them is even
+ ) => { + setState1((ev.target.value as unknown) as number); + }} + /> + ) => { + setState2((ev.target.value as unknown) as number); + }} + /> + ) => { + setState3((ev.target.value as unknown) as number); + }} + /> + {isValid !== null && {isValid ? 'Valid!' : 'Invalid'}} +
+ ); +}; +``` + +## Reference +```ts +const [validity, revalidate] = useStateValidator( + state: any[] | { [p: string]: any } | { [p: number]: any }, + validator: (state, setValidity?)=>[boolean|null, ...any[]], + initialValidity: any = [undefined] +); +``` +- **`state`**_`: any[] | { [p: string]: any } | { [p: number]: any }`_ can be both an array or object. It's _values_ will be used as a deps for inner `useEffect`. +- **`validity`**_`: [boolean|null, ...any[]]`_ result of validity check. First element is strictly nullable boolean, but others can contain arbitrary data; +- **`revalidate`**_`: ()=>void`_ runs validator once again +- **`validator`**_`: (state, setValidity?)=>[boolean|null, ...any[]]`_ should return an array suitable for validity state described above; + - `states` - current states values as the've been passed to the hook; + - `setValidity` - if defined hook will not trigger validity change automatically. Useful for async validators; +- `initialValidity` - validity value which set when validity is nt calculated yet; diff --git a/docs/useQueue.md b/docs/useQueue.md new file mode 100644 index 0000000000..ef688bb8fd --- /dev/null +++ b/docs/useQueue.md @@ -0,0 +1,26 @@ +# `useQueue` + +React state hook implements simple FIFO queue. + + +## Usage + +```jsx +import { useQueue } from 'react-use'; + +const Demo = () => { + const { add, remove, first, last, size } = useQueue(); + + return ( +
+
    +
  • first: {first}
  • +
  • last: {last}
  • +
  • size: {size}
  • +
+ + +
+ ); +}; +``` diff --git a/docs/useRefMounted.md b/docs/useRefMounted.md deleted file mode 100644 index 0e7d708eb5..0000000000 --- a/docs/useRefMounted.md +++ /dev/null @@ -1,28 +0,0 @@ -# `useRefMounted` - ->**DEPRECATED** ->This method is obsolete, use `useMountedState` instead. - -Lifecycle hook that tracks if component is mounted. Returns a ref, which has a -boolean `.current` property. - - -## Usage - -```jsx -import {useRefMounted} from 'react-use'; - -const Demo = () => { - const refMounted = useRefMounted(); - - useEffect(() => { - setTimeout(() => { - if (refMounted.current) { - // ... - } else { - // ... - } - }, 1000); - }); -}; -``` diff --git a/docs/useRendersCount.md b/docs/useRendersCount.md new file mode 100644 index 0000000000..05f6111125 --- /dev/null +++ b/docs/useRendersCount.md @@ -0,0 +1,29 @@ +# `useRendersCount` + +Tracks compontent's renders count including the first render. + +## Usage + +```typescript jsx +import * as React from 'react'; +import { useRendersCount } from "react-use"; + +const Demo = () => { + const update = useUpdate(); + const rendersCount = useRendersCount(); + + return ( +
+ Renders count: {rendersCount} +
+ +
+ ); +}; +``` + +## Reference + +```typescript +const rendersCount: number = useRendersCount(); +``` diff --git a/docs/useScrollbarWidth.md b/docs/useScrollbarWidth.md new file mode 100644 index 0000000000..b3246dfa91 --- /dev/null +++ b/docs/useScrollbarWidth.md @@ -0,0 +1,25 @@ +# `useScrollbarWidth` + +Hook that will return current browser's scrollbar width. +In case hook been called before DOM ready, it will return `undefined` and will cause re-render on first available RAF. +> **_NOTE:_** it does not work (return 0) for mobile devices, because their scrollbar width can not be determined. + +## Usage + +```jsx +const Demo = () => { + const sbw = useScrollbarWidth(); + + return ( +
+ {sbw === undefined ? `DOM is not ready yet, SBW detection delayed` : `Browser's scrollbar width is ${sbw}px`} +
+ ); +}; +``` + +## Reference + +```typescript +const sbw: number | undefined = useScrollbarWidth(); +``` diff --git a/docs/useSet.md b/docs/useSet.md new file mode 100644 index 0000000000..8b6f620ac8 --- /dev/null +++ b/docs/useSet.md @@ -0,0 +1,24 @@ +# `useSet` + +React state hook that tracks a [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set). + +## Usage + +```jsx +import {useSet} from 'react-use'; + +const Demo = () => { + const [set, { add, has, remove, reset }] = useSet(new Set(['hello'])); + + return ( +
+ + + +
{JSON.stringify(Array.from(set), null, 2)}
+
+ ); +}; +``` diff --git a/docs/useShallowCompareEffect.md b/docs/useShallowCompareEffect.md new file mode 100644 index 0000000000..4ece957c96 --- /dev/null +++ b/docs/useShallowCompareEffect.md @@ -0,0 +1,30 @@ +# `useShallowCompareEffect` + +A modified useEffect hook that is using shallow comparison on each of its dependencies instead of reference equality. + +## Usage + +```jsx +import {useCounter, useShallowCompareEffect} from 'react-use'; + +const Demo = () => { + const [count, {inc: inc}] = useCounter(0); + const options = { step: 2 }; + + useShallowCompareEffect(() => { + inc(options.step) + }, [options]); + + return ( +
+

useShallowCompareEffect: {count}

+
+ ); +}; +``` + +## Reference + +```ts +useShallowCompareEffect(effect: () => void | (() => void | undefined), deps: any[]); +``` diff --git a/docs/useStateList.md b/docs/useStateList.md index b9c91885a7..5d1fa27807 100644 --- a/docs/useStateList.md +++ b/docs/useStateList.md @@ -1,25 +1,52 @@ # `useStateList` -React state hook that circularly iterates over an array. +Provides handles for circular iteration over states list. +Supports forward and backward iterations and arbitrary position set. ## Usage ```jsx import { useStateList } from 'react-use'; +import { useRef } from 'react'; const stateSet = ['first', 'second', 'third', 'fourth', 'fifth']; const Demo = () => { - const {state, prev, next} = useStateList(stateSet); + const { state, prev, next, setStateAt, setState, currentIndex } = useStateList(stateSet); + const indexInput = useRef(null); + const stateInput = useRef(null); return (
-
{state}
+
+        {state} [index: {currentIndex}]
+      
+
+
+ + +
+ +
); }; ``` -> If the `stateSet` is changed by a shorter one the hook will select the last element of it. +## Reference + +```ts +const { state, currentIndex, prev, next, setStateAt, setState } = useStateList(stateSet: T[] = []); +``` + +If `stateSet` changed, became shorter than before and `currentIndex` left in shrinked gap - the last element of list will be taken as current. + +- **`state`**_`: T`_ — current state value; +- **`currentIndex`**_`: number`_ — current state index; +- **`prev()`**_`: void`_ — switches state to the previous one. If first element selected it will switch to the last one; +- **`nexct()`**_`: void`_ — switches state to the next one. If last element selected it will switch to the first one; +- **`setStateAt(newIndex: number)`**_`: void`_ — set the arbitrary state by index. Indexes are looped, and can be negative. +_4ex:_ if list contains 5 elements, attempt to set index 9 will bring use to the 5th element, in case of negative index it will start counting from the right, so -17 will bring us to the 4th element. +- **`setState(state: T)`**_`: void`_ — set the arbitrary state value that exists in `stateSet`. _In case new state does not exists in `stateSet` an Error will be thrown._ diff --git a/docs/useStateWithHistory.md b/docs/useStateWithHistory.md new file mode 100644 index 0000000000..5d6ac85c09 --- /dev/null +++ b/docs/useStateWithHistory.md @@ -0,0 +1,33 @@ +# `useStateHistory` + +Stores defined amount of previous state values and provides handles to travel through them. + +## Usage + +## Reference + +```typescript +const [state, setState, stateHistory] = useStateWithHistory( + initialState?: S | (()=>S), + historyCapacity?: number = 10, + initialHistory?: S +); +``` + +- **`state`**, **`setState`** and **`initialState`** are exactly the same with native React's `useState` hook; +- **`historyCapacity`** - amount of history entries that hold by storage; +- **`initialHistory`** - if defined it will be used as initial history value, otherwise history will equals `[ initialState ]`. +Initial state will not be pushed to initial history. +If entries amount is greater than `historyCapacity` parameter it wont be modified on init but will be trimmed on next `setState`; +- **`stateHistory`** - an object containing history state: + - **`history`**_`: S[]`_ - an array holding history entries. _I will have the same ref all the time so pe careful with that one!_; + - **`position`**_`: number`_ - current position _index_ in history; + - **`capacity`**_`: number = 10`_ - maximum amount of history entries; + - **`back`**_`: (amount?: number) => void`_ - go back in state history, it will cause `setState` invoke and component re-render. + If first element of history reached, the call will have no effect; + - **`forward`**_`: (amount?: number) => void`_ - go forward in state history, it will cause `setState` invoke and component re-render. + If last element of history reached, the call will have no effect; + - **`go`**_`: (position: number) => void`_ - go to arbitrary position in history. + In case `position` is non-negative ot will count elements from beginning. + Negative `position` will cause elements counting from the end, so `go(-2)` equals `go(history.length - 1)`; + diff --git a/docs/useUnmount.md b/docs/useUnmount.md index 83dc772219..c0a3e6c85b 100644 --- a/docs/useUnmount.md +++ b/docs/useUnmount.md @@ -8,7 +8,7 @@ React lifecycle hook that calls a function when the component will unmount. Use import {useUnmount} from 'react-use'; const Demo = () => { - useUnmount(() => console.log('UNMOUNTED')); + useUnmount(() => alert('UNMOUNTED')); return null; }; ``` diff --git a/docs/useUnmountPromise.md b/docs/useUnmountPromise.md new file mode 100644 index 0000000000..5a129cb936 --- /dev/null +++ b/docs/useUnmountPromise.md @@ -0,0 +1,30 @@ +# `useUnmountPromise` + +A life-cycle hook that provides a higher order promise that does not resolve if component un-mounts. + + +## Usage + +```ts +import useUnmountPromise from 'react-use/lib/useUnmountPromise'; + +const Demo = () => { + const mounted = useUnmountPromise(); + useEffect(async () => { + await mounted(someFunction()); // Will not resolve if component un-mounts. + }); +}; +``` + + +## Reference + +```ts +const mounted = useUnmountPromise(); + +mounted(promise); +mounted(promise, onError); +``` + +- `onError` — if promise rejects after the component is unmounted, `onError` + callback is called with the error. diff --git a/docs/useUpsert.md b/docs/useUpsert.md index 48c3de554c..25fe7d71fd 100644 --- a/docs/useUpsert.md +++ b/docs/useUpsert.md @@ -1,5 +1,8 @@ # `useUpsert` +> DEPRECATED! +> Use `useList` hook's upsert action instead + Superset of [`useList`](./useList.md). Provides an additional method to upsert (update or insert) an element into the list. ## Usage diff --git a/docs/useVibrate.md b/docs/useVibrate.md new file mode 100644 index 0000000000..957d01149f --- /dev/null +++ b/docs/useVibrate.md @@ -0,0 +1,31 @@ +# `useVibrate` + +React UI hook to provide physical feedback with device vibration hardware using the [Vibration API](https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API). + +## Usage + +```jsx +import {useVibrate} from 'react-use'; + +const Demo = () => { + const [vibrating, toggleVibrating] = useToggle(false); + + useVibrate(vibrating, [300, 100, 200, 100, 1000, 300], false); + + return ( +
+ +
+ ); +}; +``` + +## Reference + +```ts +useVibrate( + enabled: boolean = true, + pattern: number | number[] = [1000, 1000], + loop: boolean = true +): void; +``` diff --git a/docs/useWait.md b/docs/useWait.md deleted file mode 100644 index 99f62ae2d2..0000000000 --- a/docs/useWait.md +++ /dev/null @@ -1,37 +0,0 @@ -# `useWait` - -`useWait` is a React Hook helps to manage multiple loading states on the page without any conflict. It's based on a very simple idea that manages an `Array` of multiple loading states. The built-in `Wait` component listens its registered loader and immediately become loading state. - - -## Usage - -```jsx -import { useWait } from 'react-use' - -function UserCreateButton() { - const { startWaiting, endWaiting, isWaiting, Wait } = useWait(); - - return ( -
}> - Create User - - - ); -} -``` - -And you should wrap your `App` with `Waiter` component. It's actually a `Context.Provider` that provides a loading context to the component tree. - -```jsx -const rootElement = document.getElementById("root"); -ReactDOM.render( - - - , - rootElement -); -``` diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 0d51c47dda..0000000000 --- a/jest.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - clearMocks: true, -}; diff --git a/package.json b/package.json index ee8f3a41d2..8b03ffeb2c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-use", - "version": "12.6.0", + "version": "13.13.0", "description": "Collection of React Hooks", "main": "lib/index.js", "module": "esm/index.js", @@ -13,10 +13,10 @@ "typings": "lib/index.d.ts", "scripts": { "start": "yarn storybook", - "test": "jest", + "test": "jest --maxWorkers 2", "test:watch": "jest --watch", "test:coverage": "jest --coverage", - "lint": "tslint 'src/**/*.{ts,tsx}' -t verbose", + "lint": "tslint '{src,tests}/**/*.{ts,tsx}' -t verbose", "lint:fix": "yarn lint --fix", "lint:types": "tsc --noEmit", "build:cjs": "tsc", @@ -46,15 +46,15 @@ }, "homepage": "https://github.com/streamich/react-use#readme", "dependencies": { - "@types/react-wait": "^0.3.0", - "copy-to-clipboard": "^3.1.0", - "nano-css": "^5.1.0", + "@xobotyi/scrollbar-width": "1.5.0", + "copy-to-clipboard": "^3.2.0", + "fast-shallow-equal": "^0.1.1", + "nano-css": "^5.2.1", "react-fast-compare": "^2.0.4", - "react-wait": "^0.3.0", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.0", - "set-harmonic-interval": "^1.0.0", - "throttle-debounce": "^2.0.1", + "set-harmonic-interval": "^1.0.1", + "throttle-debounce": "^2.1.0", "ts-easing": "^0.2.0", "tslib": "^1.10.0" }, @@ -63,54 +63,55 @@ "react-dom": "^16.8.0" }, "devDependencies": { - "@babel/core": "7.6.4", - "@babel/plugin-syntax-dynamic-import": "7.2.0", - "@babel/preset-env": "7.6.3", - "@babel/preset-react": "7.6.3", - "@babel/preset-typescript": "7.6.0", - "@semantic-release/changelog": "3.0.4", - "@semantic-release/git": "7.0.16", - "@semantic-release/npm": "5.1.13", - "@shopify/jest-dom-mocks": "2.8.2", - "@storybook/addon-actions": "5.1.11", - "@storybook/addon-knobs": "5.1.11", - "@storybook/addon-notes": "5.1.11", - "@storybook/addon-options": "5.1.11", - "@storybook/react": "5.1.11", - "@testing-library/react-hooks": "2.0.3", - "@types/jest": "24.0.19", - "@types/react": "16.9.2", + "@babel/core": "7.7.7", + "@babel/plugin-syntax-dynamic-import": "7.7.4", + "@babel/preset-env": "7.7.7", + "@babel/preset-react": "7.7.4", + "@babel/preset-typescript": "7.7.7", + "@semantic-release/changelog": "3.0.6", + "@semantic-release/git": "7.0.18", + "@semantic-release/npm": "5.3.5", + "@shopify/jest-dom-mocks": "2.8.7", + "@storybook/addon-actions": "5.2.8", + "@storybook/addon-knobs": "5.2.8", + "@storybook/addon-notes": "5.2.8", + "@storybook/addon-options": "5.2.8", + "@storybook/react": "5.2.8", + "@testing-library/react-hooks": "3.2.1", + "@types/jest": "24.0.25", + "@types/react": "16.9.11", "babel-core": "6.26.3", "babel-loader": "8.0.6", "babel-plugin-dynamic-import-node": "2.3.0", - "fork-ts-checker-webpack-plugin": "1.5.1", + "fork-ts-checker-webpack-plugin": "3.1.1", "gh-pages": "2.1.1", - "husky": "3.0.9", + "husky": "3.1.0", "jest": "24.9.0", "keyboardjs": "2.5.1", - "lint-staged": "9.4.2", + "lint-staged": "9.5.0", "markdown-loader": "5.1.0", - "prettier": "1.18.2", + "prettier": "1.19.1", "raf-stub": "3.0.0", - "react": "16.10.2", - "react-dom": "16.10.2", + "react": "16.12.0", + "react-dom": "16.12.0", "react-frame-component": "4.1.1", "react-spring": "8.0.27", - "react-test-renderer": "16.10.2", + "react-test-renderer": "16.12.0", "rebound": "0.1.0", "redux-logger": "3.0.6", "redux-thunk": "2.3.0", "rimraf": "3.0.0", - "rxjs": "6.5.3", - "semantic-release": "15.13.24", - "ts-loader": "6.2.0", - "ts-node": "8.4.1", - "tslint": "5.20.0", + "rxjs": "6.5.4", + "semantic-release": "15.14.0", + "ts-jest": "24.2.0", + "ts-loader": "6.2.1", + "ts-node": "8.5.4", + "tslint": "6.0.0-beta1", "tslint-config-prettier": "1.18.0", "tslint-eslint-rules": "5.4.0", - "tslint-plugin-prettier": "2.0.1", + "tslint-plugin-prettier": "2.1.0", "tslint-react": "4.1.0", - "typescript": "3.5.3" + "typescript": "3.7.4" }, "config": { "commitizen": { @@ -143,11 +144,19 @@ ] }, "volta": { - "node": "10.16.3", - "yarn": "1.19.1" + "node": "10.18.0", + "yarn": "1.21.1" }, "collective": { "type": "opencollective", "url": "https://opencollective.com/react-use" + }, + "jest": { + "preset": "ts-jest", + "clearMocks": true, + "coverageDirectory": "coverage", + "testMatch": [ + "/tests/**/*.test.(ts|tsx)" + ] } } diff --git a/renovate.json b/renovate.json index 3d1bae418d..37ab773ab4 100644 --- a/renovate.json +++ b/renovate.json @@ -4,6 +4,7 @@ ], "automerge": true, "pinVersions": false, + "ignoreUnstable": true, "major": { "automerge": false }, diff --git a/src/__stories__/useList.story.tsx b/src/__stories__/useList.story.tsx deleted file mode 100644 index 2ccfb611de..0000000000 --- a/src/__stories__/useList.story.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { storiesOf } from '@storybook/react'; -import * as React from 'react'; -import { useList } from '..'; -import ShowDocs from './util/ShowDocs'; - -const Demo = () => { - const [list, { clear, filter, push, remove, set, sort, updateAt, reset }] = useList([1, 2, 3, 4, 5]); - - return ( -
- - - - - - - - - -
{JSON.stringify(list, null, 2)}
-
- ); -}; - -storiesOf('State|useList', module) - .add('Docs', () => ) - .add('Demo', () => ); diff --git a/src/__stories__/useMount.story.tsx b/src/__stories__/useMount.story.tsx deleted file mode 100644 index 966ab737e5..0000000000 --- a/src/__stories__/useMount.story.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { storiesOf } from '@storybook/react'; -import * as React from 'react'; -import { useMount } from '..'; -import ConsoleStory from './util/ConsoleStory'; -import ShowDocs from './util/ShowDocs'; - -const Demo = () => { - useMount(() => console.log('MOUNTED')); - - return ; -}; - -storiesOf('Lifecycle|useMount', module) - .add('Docs', () => ) - .add('Demo', () => ); diff --git a/src/__stories__/useRefMounted.story.tsx b/src/__stories__/useRefMounted.story.tsx deleted file mode 100644 index 5a8c22b6a9..0000000000 --- a/src/__stories__/useRefMounted.story.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { storiesOf } from '@storybook/react'; -import * as React from 'react'; -import { useRaf, useRefMounted } from '..'; -import ShowDocs from './util/ShowDocs'; - -const Demo = () => { - const refMounted = useRefMounted(); - - useRaf(); - return ( -
-

**DEPRECATED**

-

This method is obsolete, use `useMountedState` instead.

- is mounted: {refMounted.current ? '👍' : '👎'} -
- ); -}; - -storiesOf('Lifecycle|useRefMounted', module) - .add('Docs', () => ) - .add('Demo', () => ); diff --git a/src/__stories__/useStateList.story.tsx b/src/__stories__/useStateList.story.tsx deleted file mode 100644 index 219f753139..0000000000 --- a/src/__stories__/useStateList.story.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { storiesOf } from '@storybook/react'; -import * as React from 'react'; -import { useStateList } from '..'; -import ShowDocs from './util/ShowDocs'; - -const stateSet = ['first', 'second', 'third', 'fourth', 'fifth']; - -const Demo = () => { - const { state, prev, next } = useStateList(stateSet); - - return ( -
-
{state}
- - -
- ); -}; - -storiesOf('State|useStateList', module) - .add('Docs', () => ) - .add('Demo', () => ); diff --git a/src/__stories__/useUnmount.story.tsx b/src/__stories__/useUnmount.story.tsx deleted file mode 100644 index 87d58e3595..0000000000 --- a/src/__stories__/useUnmount.story.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { storiesOf } from '@storybook/react'; -import * as React from 'react'; -import { useUnmount } from '..'; -import ConsoleStory from './util/ConsoleStory'; -import ShowDocs from './util/ShowDocs'; - -const Demo = () => { - useUnmount(() => console.log('UNMOUNTED')); - - return ; -}; - -storiesOf('Lifecycle|useUnmount', module) - .add('Docs', () => ) - .add('Demo', () => ); diff --git a/src/__stories__/useWait.story.tsx b/src/__stories__/useWait.story.tsx deleted file mode 100644 index f7bd67ae86..0000000000 --- a/src/__stories__/useWait.story.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { storiesOf } from '@storybook/react'; -import * as React from 'react'; -import { useWait } from '..'; -import ShowDocs from './util/ShowDocs'; - -const AnotherComponent = () => { - const { isWaiting } = useWait(); - return

{isWaiting('creating user') ? 'Now creating user...' : ''}

; -}; - -const Demo = () => { - const { Wait, isWaiting, startWaiting, endWaiting } = useWait(); - - function createUser() { - startWaiting('creating user'); - setTimeout(() => { - endWaiting('creating user'); - }, 1000); - } - - return ( -
- - -
- ); -}; - -storiesOf('UI|useWait', module) - .add('Docs', () => ) - .add('Demo', () => ( - - - - )); diff --git a/src/__tests__/useList.test.ts b/src/__tests__/useList.test.ts deleted file mode 100644 index 47b5beb73b..0000000000 --- a/src/__tests__/useList.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { act, renderHook } from '@testing-library/react-hooks'; -import useList from '../useList'; - -const setUp = (initialList?: any[]) => renderHook(() => useList(initialList)); - -it('should init list and utils', () => { - const { result } = setUp([1, 2, 3]); - const [list, utils] = result.current; - - expect(list).toEqual([1, 2, 3]); - expect(utils).toStrictEqual({ - set: expect.any(Function), - clear: expect.any(Function), - updateAt: expect.any(Function), - remove: expect.any(Function), - push: expect.any(Function), - filter: expect.any(Function), - sort: expect.any(Function), - reset: expect.any(Function), - }); -}); - -it('should init empty list if not initial list provided', () => { - const { result } = setUp(); - - expect(result.current[0]).toEqual([]); -}); - -it('should set new list', () => { - const initList = [1, 2, 3]; - const { result } = setUp(initList); - const [, utils] = result.current; - - act(() => { - utils.set([4, 5, 6]); - }); - - expect(result.current[0]).toEqual([4, 5, 6]); - expect(result.current[0]).not.toBe(initList); // checking immutability -}); - -it('should clear current list', () => { - const initList = [1, 2, 3]; - const { result } = setUp(initList); - const [, utils] = result.current; - - act(() => { - utils.clear(); - }); - - expect(result.current[0]).toEqual([]); - expect(result.current[0]).not.toBe(initList); // checking immutability -}); - -it('should update element at specific position', () => { - const initList = [1, 2, 3]; - const { result } = setUp(initList); - const [, utils] = result.current; - - act(() => { - utils.updateAt(1, 'foo'); - }); - - expect(result.current[0]).toEqual([1, 'foo', 3]); - expect(result.current[0]).not.toBe(initList); // checking immutability -}); - -it('should remove element at specific position', () => { - const initList = [1, 2, 3]; - const { result } = setUp(initList); - const [, utils] = result.current; - - act(() => { - utils.remove(1); - }); - - expect(result.current[0]).toEqual([1, 3]); - expect(result.current[0]).not.toBe(initList); // checking immutability -}); - -it('should push new element at the end of the list', () => { - const initList = [1, 2, 3]; - const { result } = setUp(initList); - const [, utils] = result.current; - - act(() => { - utils.push(0); - }); - - expect(result.current[0]).toEqual([1, 2, 3, 0]); - expect(result.current[0]).not.toBe(initList); // checking immutability -}); - -it('should push duplicated element at the end of the list', () => { - const initList = [1, 2, 3]; - const { result } = setUp(initList); - const [, utils] = result.current; - - act(() => { - utils.push(2); - }); - - expect(result.current[0]).toEqual([1, 2, 3, 2]); - expect(result.current[0]).not.toBe(initList); // checking immutability -}); - -it('should push multiple elements at the end of the list', () => { - const initList = [1, 2, 3]; - const { result } = setUp(initList); - const [, utils] = result.current; - - act(() => { - utils.push(4, 5, 6); - }); - - expect(result.current[0]).toEqual([1, 2, 3, 4, 5, 6]); - expect(result.current[0]).not.toBe(initList); // checking immutability -}); - -it('should filter current list by provided function', () => { - const initList = [1, -1, 2, -2, 3, -3]; - const { result } = setUp(initList); - const [, utils] = result.current; - - act(() => { - utils.filter(n => n < 0); - }); - - expect(result.current[0]).toEqual([-1, -2, -3]); - expect(result.current[0]).not.toBe(initList); // checking immutability -}); - -it('should sort current list by default order', () => { - const initList = ['March', 'Jan', 'Feb', 'Dec']; - const { result } = setUp(initList); - const [, utils] = result.current; - - act(() => { - utils.sort(); - }); - - expect(result.current[0]).toEqual(['Dec', 'Feb', 'Jan', 'March']); - expect(result.current[0]).not.toBe(initList); // checking immutability -}); - -it('should sort current list by provided function', () => { - const initList = ['March', 'Jan', 'Feb', 'Dec']; - const { result } = setUp(initList); - const [, utils] = result.current; - - act(() => { - utils.sort((a, b) => { - if (a < b) { - return 1; - } - if (a > b) { - return -1; - } - - return 0; - }); - }); - - expect(result.current[0]).toEqual(['March', 'Jan', 'Feb', 'Dec']); - expect(result.current[0]).not.toBe(initList); // checking immutability -}); - -it('should reset the list to initial list provided', () => { - const initList = [1, 2, 3]; - const { result } = setUp(initList); - const [, utils] = result.current; - - act(() => { - utils.push(4); - }); - - expect(result.current[0]).toEqual([1, 2, 3, 4]); - - act(() => { - utils.reset(); - }); - - expect(result.current[0]).toEqual([1, 2, 3]); - expect(result.current[0]).not.toBe(initList); // checking immutability -}); - -it('should memoized its utils methods', () => { - const initList = [1, 2, 3]; - const { result } = setUp(initList); - const [, utils] = result.current; - const { set, clear, updateAt, remove, push, filter, sort, reset } = utils; - - act(() => { - push(4); - }); - - expect(result.current[1]).toBe(utils); - expect(result.current[1].set).toBe(set); - expect(result.current[1].clear).toBe(clear); - expect(result.current[1].updateAt).toBe(updateAt); - expect(result.current[1].remove).toBe(remove); - expect(result.current[1].push).toBe(push); - expect(result.current[1].filter).toBe(filter); - expect(result.current[1].sort).toBe(sort); - expect(result.current[1].reset).toBe(reset); -}); diff --git a/src/__tests__/usePreviousDistinct.test.tsx b/src/__tests__/usePreviousDistinct.test.tsx deleted file mode 100644 index 073c86b11c..0000000000 --- a/src/__tests__/usePreviousDistinct.test.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks'; -import usePreviousDistinct from '../usePreviousDistinct'; - -describe('usePreviousDistinct with default compare', () => { - const hook = renderHook(props => usePreviousDistinct(props), { initialProps: 0 }); - - it('should return undefined on initial render', () => { - expect(hook.result.current).toBe(undefined); - }); - - it('should return previous state only after a different value is rendered', () => { - expect(hook.result.current).toBeUndefined(); - hook.rerender(1); - expect(hook.result.current).toBe(0); - hook.rerender(2); - hook.rerender(2); - expect(hook.result.current).toBe(1); - - hook.rerender(3); - expect(hook.result.current).toBe(2); - }); -}); - -describe('usePreviousDistinct with complex comparison', () => { - const exampleObjects = [ - { - id: 'something-unique', - name: 'Nancy', - }, - { - id: 'something-unique2', - name: 'Fred', - }, - { - id: 'something-unique3', - name: 'Bill', - }, - { - id: 'something-unique4', - name: 'Alice', - }, - ]; - const hook = renderHook( - props => usePreviousDistinct(props, (prev, next) => (prev && prev.id) === (next && next.id)), - { - initialProps: exampleObjects[0], - } - ); - - it('should return undefined on initial render', () => { - expect(hook.result.current).toBe(undefined); - }); - - it('should return previous state only after a different value is rendered', () => { - expect(hook.result.current).toBeUndefined(); - hook.rerender(exampleObjects[1]); - expect(hook.result.current).toMatchObject(exampleObjects[0]); - hook.rerender(exampleObjects[2]); - hook.rerender(exampleObjects[2]); - expect(hook.result.current).toMatchObject(exampleObjects[1]); - - hook.rerender(exampleObjects[3]); - expect(hook.result.current).toMatchObject(exampleObjects[2]); - }); -}); diff --git a/src/__tests__/useStateList.test.ts b/src/__tests__/useStateList.test.ts deleted file mode 100644 index b65d324173..0000000000 --- a/src/__tests__/useStateList.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { renderHook, act } from '@testing-library/react-hooks'; -import useStateList from '../useStateList'; - -const callNext = hook => { - act(() => { - const { next } = hook.result.current; - next(); - }); -}; - -const callPrev = hook => { - act(() => { - const { prev } = hook.result.current; - prev(); - }); -}; - -describe('happy flow', () => { - const hook = renderHook(({ stateSet }) => useStateList(stateSet), { - initialProps: { - stateSet: ['a', 'b', 'c'], - }, - }); - - it('should return the first state on initial render', () => { - const { state } = hook.result.current; - expect(state).toBe('a'); - }); - - it('should return the second state after calling the "next" function', () => { - callNext(hook); - - const { state } = hook.result.current; - expect(state).toBe('b'); - }); - - it('should return the first state again after calling the "next" function "stateSet.length" times', () => { - callNext(hook); - callNext(hook); - - const { state } = hook.result.current; - expect(state).toBe('a'); - }); - - it('should return the last state again after calling the "prev" function', () => { - callPrev(hook); - - const { state } = hook.result.current; - expect(state).toBe('c'); - }); - - it('should return the previous state after calling the "prev" function', () => { - callPrev(hook); - - const { state } = hook.result.current; - expect(state).toBe('b'); - }); -}); - -describe('with empty state set', () => { - const hook = renderHook(({ stateSet }) => useStateList(stateSet), { - initialProps: { - stateSet: [], - }, - }); - - it('should return undefined on initial render', () => { - const { state } = hook.result.current; - expect(state).toBe(undefined); - }); - - it('should always return undefined (calling next)', () => { - callNext(hook); - - const { state } = hook.result.current; - expect(state).toBe(undefined); - }); - - it('should always return undefined (calling prev)', () => { - callPrev(hook); - - const { state } = hook.result.current; - expect(state).toBe(undefined); - }); -}); - -describe('with a single state set', () => { - const hook = renderHook(({ stateSet }) => useStateList(stateSet), { - initialProps: { - stateSet: ['a'], - }, - }); - - it('should return "a" on initial render', () => { - const { state } = hook.result.current; - expect(state).toBe('a'); - }); - - it('should always return "a" (calling next)', () => { - callNext(hook); - - const { state } = hook.result.current; - expect(state).toBe('a'); - }); - - it('should always return "a" (calling prev)', () => { - callPrev(hook); - - const { state } = hook.result.current; - expect(state).toBe('a'); - }); -}); - -describe('with stateSet updates', () => { - const hook = renderHook(({ stateSet }) => useStateList(stateSet), { - initialProps: { - stateSet: ['a', 'c', 'b', 'f', 'g'], - }, - }); - - it('should return the last element after updating with a shorter state set', () => { - // Go to the 4th state - callNext(hook); // c - callNext(hook); // b - callNext(hook); // f - - // Update the state set with less elements - hook.rerender({ - stateSet: ['a', 'c'], - }); - - const { state } = hook.result.current; - expect(state).toBe('c'); - }); - - it('should return the element in the same position after updating with a larger state set', () => { - hook.rerender({ - stateSet: ['a', 'f', 'l'], - }); - - const { state } = hook.result.current; - expect(state).toBe('f'); - }); -}); diff --git a/src/__tests__/useUnmount.test.ts b/src/__tests__/useUnmount.test.ts deleted file mode 100644 index ae713baa2c..0000000000 --- a/src/__tests__/useUnmount.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks'; -import { useUnmount } from '..'; - -const mockCallback = jest.fn(); - -afterEach(() => { - jest.resetAllMocks(); -}); - -it('should not call provided callback on mount', () => { - renderHook(() => useUnmount(mockCallback)); - - expect(mockCallback).not.toHaveBeenCalled(); -}); - -it('should call provided callback on unmount', () => { - const { unmount } = renderHook(() => useUnmount(mockCallback)); - expect(mockCallback).not.toHaveBeenCalled(); - - unmount(); - - expect(mockCallback).toHaveBeenCalledTimes(1); -}); - -it('should not call provided callback on rerender', () => { - const { rerender } = renderHook(() => useUnmount(mockCallback)); - expect(mockCallback).not.toHaveBeenCalled(); - - rerender(); - - expect(mockCallback).not.toHaveBeenCalled(); -}); diff --git a/src/__tests__/useWindowSize.test.tsx b/src/__tests__/useWindowSize.test.tsx deleted file mode 100644 index 015cf495c2..0000000000 --- a/src/__tests__/useWindowSize.test.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { act, renderHook } from '@testing-library/react-hooks'; -import { replaceRaf } from 'raf-stub'; -import useWindowSize from '../useWindowSize'; - -interface RequestAnimationFrame { - reset(): void; - step(): void; -} - -declare var requestAnimationFrame: RequestAnimationFrame; - -replaceRaf(); - -beforeEach(() => { - requestAnimationFrame.reset(); -}); - -afterEach(() => { - requestAnimationFrame.reset(); -}); - -// simulate window resize -function fireResize(type, value) { - switch (type) { - case 'width': - (window.innerWidth as number) = value; // assert type of window.innerWidth as it is typed as readonly. - break; - case 'height': - (window.innerHeight as number) = value; // assert type of window.innerHeight as it is typed as readonly. - break; - default: - break; - } - - window.dispatchEvent(new Event('resize')); -} - -describe('useWindowSize', () => { - it('should be defined', () => { - expect(useWindowSize).toBeDefined(); - }); - - const hook = renderHook(() => useWindowSize()); - - it('should update width', () => { - act(() => { - fireResize('width', 320); - requestAnimationFrame.step(); - }); - - expect(hook.result.current.width).toBe(320); - - act(() => { - fireResize('width', 640); - requestAnimationFrame.step(); - }); - expect(hook.result.current.width).toBe(640); - }); - - it('should update height', () => { - act(() => { - fireResize('height', 500); - requestAnimationFrame.step(); - }); - expect(hook.result.current.height).toBe(500); - - act(() => { - fireResize('height', 1000); - requestAnimationFrame.step(); - }); - expect(hook.result.current.height).toBe(1000); - }); -}); diff --git a/src/createBreakpoint.ts b/src/createBreakpoint.ts new file mode 100644 index 0000000000..eee72085ee --- /dev/null +++ b/src/createBreakpoint.ts @@ -0,0 +1,31 @@ +import { useEffect, useState, useMemo } from 'react'; + +const createBreakpoint = ( + breakpoints: { [name: string]: number } = { laptopL: 1440, laptop: 1024, tablet: 768 } +) => () => { + const [screen, setScreen] = useState(0); + + useEffect(() => { + const setSideScreen = (): void => { + setScreen(window.innerWidth); + }; + setSideScreen(); + window.addEventListener('resize', setSideScreen); + return () => { + window.removeEventListener('resize', setSideScreen); + }; + }); + const sortedBreakpoints = useMemo(() => Object.entries(breakpoints).sort((a, b) => (a[1] >= b[1] ? 1 : -1)), [ + breakpoints, + ]); + const result = sortedBreakpoints.reduce((acc, [name, width]) => { + if (screen >= width) { + return name; + } else { + return acc; + } + }, sortedBreakpoints[0][0]); + return result; +}; + +export default createBreakpoint; diff --git a/src/createReducer.ts b/src/createReducer.ts index 781900f45a..50b8ec06c7 100644 --- a/src/createReducer.ts +++ b/src/createReducer.ts @@ -1,18 +1,31 @@ -import { useCallback, useRef, useState } from 'react'; +import { MutableRefObject, useCallback, useRef, useState } from 'react'; import useUpdateEffect from './useUpdateEffect'; -function composeMiddleware(chain) { - return (context, dispatch) => { +type Dispatch = (action: Action) => void; + +interface Store { + getState: () => State; + dispatch: Dispatch; +} + +type Middleware = (store: Store) => (next: Dispatch) => (action: Action) => void; + +function composeMiddleware(chain: Middleware[]) { + return (context: Store, dispatch: Dispatch) => { return chain.reduceRight((res, middleware) => { return middleware(context)(res); }, dispatch); }; } -const createReducer = (...middlewares) => { - const composedMiddleware = composeMiddleware(middlewares); +const createReducer = (...middlewares: Middleware[]) => { + const composedMiddleware = composeMiddleware(middlewares); - return (reducer, initialState, initializer = value => value) => { + return ( + reducer: (state: State, action: Action) => State, + initialState: State, + initializer = (value: State) => value + ): [State, Dispatch] => { const ref = useRef(initializer(initialState)); const [, setState] = useState(ref.current); @@ -25,11 +38,11 @@ const createReducer = (...middlewares) => { [reducer] ); - const dispatchRef = useRef( + const dispatchRef: MutableRefObject> = useRef( composedMiddleware( { getState: () => ref.current, - dispatch: (...args) => dispatchRef.current(...args), + dispatch: (...args: [Action]) => dispatchRef.current(...args), }, dispatch ) @@ -39,7 +52,7 @@ const createReducer = (...middlewares) => { dispatchRef.current = composedMiddleware( { getState: () => ref.current, - dispatch: (...args) => dispatchRef.current(...args), + dispatch: (...args: [Action]) => dispatchRef.current(...args), }, dispatch ); diff --git a/src/index.ts b/src/index.ts index ae7abd5204..4a0aee4fa7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,12 +11,14 @@ export { default as useClickAway } from './useClickAway'; export { default as useCopyToClipboard } from './useCopyToClipboard'; export { default as useCounter } from './useCounter'; export { default as useCss } from './useCss'; +export { default as useCustomCompareEffect } from './useCustomCompareEffect'; export { default as useDebounce } from './useDebounce'; export { default as useDeepCompareEffect } from './useDeepCompareEffect'; export { default as useDefault } from './useDefault'; export { default as useDrop } from './useDrop'; export { default as useDropArea } from './useDropArea'; export { default as useEffectOnce } from './useEffectOnce'; +export { default as useEnsuredForwardedRef, ensuredForwardRef } from './useEnsuredForwardedRef'; export { default as useEvent } from './useEvent'; export { default as useFavicon } from './useFavicon'; export { default as useFullscreen } from './useFullscreen'; @@ -31,6 +33,7 @@ export { default as useIntersection } from './useIntersection'; export { default as useInterval } from './useInterval'; export { default as useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; export { default as useKey } from './useKey'; +export { default as createBreakpoint } from './createBreakpoint'; // not exported because of peer dependency // export { default as useKeyboardJs } from './useKeyboardJs'; export { default as useKeyPress } from './useKeyPress'; @@ -44,6 +47,7 @@ export { default as useLogger } from './useLogger'; export { default as useMap } from './useMap'; export { default as useMedia } from './useMedia'; export { default as useMediaDevices } from './useMediaDevices'; +export { useMediatedState } from './useMediatedState'; export { default as useMotion } from './useMotion'; export { default as useMount } from './useMount'; export { default as useMountedState } from './useMountedState'; @@ -58,25 +62,23 @@ export { default as usePermission } from './usePermission'; export { default as usePrevious } from './usePrevious'; export { default as usePreviousDistinct } from './usePreviousDistinct'; export { default as usePromise } from './usePromise'; +export { default as useQueue } from './useQueue'; export { default as useRaf } from './useRaf'; export { default as useRafLoop } from './useRafLoop'; export { default as useRafState } from './useRafState'; - -/** - * @deprecated This hook is obsolete, use `useMountedState` instead - */ -export { default as useRefMounted } from './useRefMounted'; export { default as useSearchParam } from './useSearchParam'; export { default as useScroll } from './useScroll'; export { default as useScrolling } from './useScrolling'; export { default as useSessionStorage } from './useSessionStorage'; export { default as useSetState } from './useSetState'; +export { default as useShallowCompareEffect } from './useShallowCompareEffect'; export { default as useSize } from './useSize'; export { default as useSlider } from './useSlider'; export { default as useSpeech } from './useSpeech'; // not exported because of peer dependency // export { default as useSpring } from './useSpring'; export { default as useStartTyping } from './useStartTyping'; +export { useStateWithHistory } from './useStateWithHistory'; export { default as useStateList } from './useStateList'; export { default as useThrottle } from './useThrottle'; export { default as useThrottleFn } from './useThrottleFn'; @@ -86,12 +88,18 @@ export { default as useTitle } from './useTitle'; export { default as useToggle } from './useToggle'; export { default as useTween } from './useTween'; export { default as useUnmount } from './useUnmount'; +export { default as useUnmountPromise } from './useUnmountPromise'; export { default as useUpdate } from './useUpdate'; export { default as useUpdateEffect } from './useUpdateEffect'; export { default as useUpsert } from './useUpsert'; +export { default as useVibrate } from './useVibrate'; export { default as useVideo } from './useVideo'; export { default as useStateValidator } from './useStateValidator'; -export { useWait, Waiter } from './useWait'; +export { useScrollbarWidth } from './useScrollbarWidth'; +export { useMultiStateValidator } from './useMultiStateValidator'; export { default as useWindowScroll } from './useWindowScroll'; export { default as useWindowSize } from './useWindowSize'; export { default as useMeasure } from './useMeasure'; +export { useRendersCount } from './useRendersCount'; +export { useFirstMountState } from './useFirstMountState'; +export { default as useSet } from './useSet'; diff --git a/src/useAsyncFn.ts b/src/useAsyncFn.ts index dd2fc33e9f..0b0cd74875 100644 --- a/src/useAsyncFn.ts +++ b/src/useAsyncFn.ts @@ -1,4 +1,4 @@ -import { DependencyList, useCallback, useState } from 'react'; +import { DependencyList, useCallback, useState, useRef } from 'react'; import useMountedState from './useMountedState'; export type AsyncState = @@ -28,21 +28,23 @@ export default function useAsyncFn( deps: DependencyList = [], initialState: AsyncState = { loading: false } ): AsyncFn { + const lastCallId = useRef(0); const [state, set] = useState>(initialState); const isMounted = useMountedState(); const callback = useCallback((...args: Args | []) => { + const callId = ++lastCallId.current; set({ loading: true }); return fn(...args).then( value => { - isMounted() && set({ value, loading: false }); + isMounted() && callId === lastCallId.current && set({ value, loading: false }); return value; }, error => { - isMounted() && set({ error, loading: false }); + isMounted() && callId === lastCallId.current && set({ error, loading: false }); return error; } diff --git a/src/useBattery.ts b/src/useBattery.ts index 8be532a435..40e085b2d9 100644 --- a/src/useBattery.ts +++ b/src/useBattery.ts @@ -25,7 +25,7 @@ interface NavigatorWithPossibleBattery extends Navigator { type UseBatteryState = | { isSupported: false } // Battery API is not supported | { isSupported: true; fetched: false } // battery API supported but not fetched yet - | BatteryState & { isSupported: true; fetched: true }; // battery API supported and fetched + | (BatteryState & { isSupported: true; fetched: true }); // battery API supported and fetched const nav: NavigatorWithPossibleBattery | undefined = typeof navigator === 'object' ? navigator : undefined; const isBatteryApiSupported = nav && typeof nav.getBattery === 'function'; diff --git a/src/useCounter.ts b/src/useCounter.ts index 0c3fbae014..03c0ad36b4 100644 --- a/src/useCounter.ts +++ b/src/useCounter.ts @@ -1,85 +1,89 @@ -import { useCallback } from 'react'; +import { useMemo } from 'react'; import useGetSet from './useGetSet'; +import { HookState, InitialHookState, resolveHookState } from './util/resolveHookState'; export interface CounterActions { inc: (delta?: number) => void; dec: (delta?: number) => void; get: () => number; - set: (value: number) => void; - reset: (value?: number) => void; + set: (value: HookState) => void; + reset: (value?: HookState) => void; } export default function useCounter( - initialValue: number = 0, + initialValue: InitialHookState = 0, max: number | null = null, min: number | null = null ): [number, CounterActions] { - typeof initialValue !== 'number' && console.error('initialValue has to be a number, got ' + typeof initialValue); + let init = resolveHookState(initialValue); + + typeof init !== 'number' && console.error('initialValue has to be a number, got ' + typeof initialValue); if (typeof min === 'number') { - initialValue = Math.max(initialValue, min); + init = Math.max(init, min); } else if (min !== null) { console.error('min has to be a number, got ' + typeof min); } if (typeof max === 'number') { - initialValue = Math.min(initialValue, max); + init = Math.min(init, max); } else if (max !== null) { console.error('max has to be a number, got ' + typeof max); } - const [get, setInternal] = useGetSet(initialValue); + const [get, setInternal] = useGetSet(init); - function set(value: number): void { - const current = get(); + return [ + get(), + useMemo(() => { + const set = (newState: HookState) => { + const prevState = get(); + let rState = resolveHookState(newState, prevState); - if (current === value) { - return; - } + if (prevState !== rState) { + if (typeof min === 'number') { + rState = Math.max(rState, min); + } + if (typeof max === 'number') { + rState = Math.min(rState, max); + } - if (typeof min === 'number') { - value = Math.max(value, min); - } - if (typeof max === 'number') { - value = Math.min(value, max); - } + prevState !== rState && setInternal(rState); + } + }; - current !== value && setInternal(value); - } + return { + get, + set, + inc: (delta: HookState = 1) => { + const rDelta = resolveHookState(delta, get()); + + if (typeof rDelta !== 'number') { + console.error('delta has to be a number or function returning a number, got ' + typeof rDelta); + } + + set((num: number) => num + rDelta); + }, + dec: (delta: HookState = 1) => { + const rDelta = resolveHookState(delta, get()); + + if (typeof rDelta !== 'number') { + console.error('delta has to be a number or function returning a number, got ' + typeof rDelta); + } + + set((num: number) => num - rDelta); + }, + reset: (value: HookState = init) => { + const rValue = resolveHookState(value, get()); + + if (typeof rValue !== 'number') { + console.error('value has to be a number or function returning a number, got ' + typeof rValue); + } - const inc = useCallback( - (delta: number = 1) => { - typeof delta !== 'number' && console.error('delta has to be a number, got ' + typeof delta); - - set(get() + delta); - }, - [max, min] - ); - const dec = useCallback( - (delta: number = 1) => { - typeof delta !== 'number' && console.error('delta has to be a number, got ' + typeof delta); - - set(get() - delta); - }, - [max, min] - ); - const reset = useCallback( - (value: number = initialValue) => { - typeof value !== 'number' && console.error('value has to be a number, got ' + typeof value); - - initialValue = value; - set(value); - }, - [max, min] - ); - - const actions = { - inc, - dec, - get, - set, - reset, - }; - - return [get(), actions]; + init = rValue; + set(rValue); + }, + }; + }, [init, min, max]), + ]; } diff --git a/src/useCustomCompareEffect.ts b/src/useCustomCompareEffect.ts new file mode 100644 index 0000000000..8f189188e4 --- /dev/null +++ b/src/useCustomCompareEffect.ts @@ -0,0 +1,33 @@ +import { DependencyList, EffectCallback, useEffect, useRef } from 'react'; + +const isPrimitive = (val: any) => val !== Object(val); + +type DepsEqualFnType = (prevDeps: DependencyList, nextDeps: DependencyList) => boolean; + +const useCustomCompareEffect = (effect: EffectCallback, deps: DependencyList, depsEqual: DepsEqualFnType) => { + if (process.env.NODE_ENV !== 'production') { + if (!(deps instanceof Array) || !deps.length) { + console.warn('`useCustomCompareEffect` should not be used with no dependencies. Use React.useEffect instead.'); + } + + if (deps.every(isPrimitive)) { + console.warn( + '`useCustomCompareEffect` should not be used with dependencies that are all primitive values. Use React.useEffect instead.' + ); + } + + if (typeof depsEqual !== 'function') { + console.warn('`useCustomCompareEffect` should be used with depsEqual callback for comparing deps list'); + } + } + + const ref = useRef(undefined); + + if (!ref.current || !depsEqual(deps, ref.current)) { + ref.current = deps; + } + + useEffect(effect, ref.current); +}; + +export default useCustomCompareEffect; diff --git a/src/useDebounce.ts b/src/useDebounce.ts index bb7bffb7af..3043eecd60 100644 --- a/src/useDebounce.ts +++ b/src/useDebounce.ts @@ -1,14 +1,12 @@ -import { DependencyList } from 'react'; -import useUpdateEffect from './useUpdateEffect'; +import { DependencyList, useEffect } from 'react'; +import useTimeoutFn from './useTimeoutFn'; -const useDebounce = (fn: () => any, ms: number = 0, deps: DependencyList = []) => { - useUpdateEffect(() => { - const timeout = setTimeout(fn, ms); +export type UseDebounceReturn = [() => boolean | null, () => void]; - return () => { - clearTimeout(timeout); - }; - }, deps); -}; +export default function useDebounce(fn: Function, ms: number = 0, deps: DependencyList = []): UseDebounceReturn { + const [isReady, cancel, reset] = useTimeoutFn(fn, ms); -export default useDebounce; + useEffect(reset, deps); + + return [isReady, cancel]; +} diff --git a/src/useDeepCompareEffect.ts b/src/useDeepCompareEffect.ts index e4991e3532..df57b8d169 100644 --- a/src/useDeepCompareEffect.ts +++ b/src/useDeepCompareEffect.ts @@ -1,11 +1,12 @@ -import { DependencyList, EffectCallback, useEffect, useRef } from 'react'; +import { DependencyList, EffectCallback } from 'react'; import isEqual from 'react-fast-compare'; +import useCustomCompareEffect from './useCustomCompareEffect'; const isPrimitive = (val: any) => val !== Object(val); -const useDeepCompareEffect = (effect: EffectCallback, deps: any[]) => { +const useDeepCompareEffect = (effect: EffectCallback, deps: DependencyList) => { if (process.env.NODE_ENV !== 'production') { - if (!deps || !deps.length) { + if (!(deps instanceof Array) || !deps.length) { console.warn('`useDeepCompareEffect` should not be used with no dependencies. Use React.useEffect instead.'); } @@ -16,13 +17,7 @@ const useDeepCompareEffect = (effect: EffectCallback, deps: any[]) => { } } - const ref = useRef(undefined); - - if (!isEqual(deps, ref.current)) { - ref.current = deps; - } - - useEffect(effect, ref.current); + useCustomCompareEffect(effect, deps, isEqual); }; export default useDeepCompareEffect; diff --git a/src/useEnsuredForwardedRef.ts b/src/useEnsuredForwardedRef.ts new file mode 100644 index 0000000000..d7451e55a4 --- /dev/null +++ b/src/useEnsuredForwardedRef.ts @@ -0,0 +1,33 @@ +import { + forwardRef, + useRef, + useEffect, + MutableRefObject, + ForwardRefExoticComponent, + PropsWithoutRef, + RefAttributes, + RefForwardingComponent, + PropsWithChildren, +} from 'react'; + +export default function useEnsuredForwardedRef(forwardedRef: MutableRefObject): MutableRefObject { + const ensuredRef = useRef(forwardedRef && forwardedRef.current); + + useEffect(() => { + if (!forwardedRef) { + return; + } + forwardedRef.current = ensuredRef.current; + }, [forwardedRef]); + + return ensuredRef; +} + +export function ensuredForwardRef( + Component: RefForwardingComponent +): ForwardRefExoticComponent & RefAttributes> { + return forwardRef((props: PropsWithChildren

, ref) => { + const ensuredRef = useEnsuredForwardedRef(ref as MutableRefObject); + return Component(props, ensuredRef); + }); +} diff --git a/src/useEvent.ts b/src/useEvent.ts index 3074df6ddc..b9c1ec4c36 100644 --- a/src/useEvent.ts +++ b/src/useEvent.ts @@ -3,25 +3,32 @@ import { isClient } from './util'; export interface ListenerType1 { addEventListener(name: string, handler: (event?: any) => void, ...args: any[]); - - removeEventListener(name: string, handler: (event?: any) => void); + removeEventListener(name: string, handler: (event?: any) => void, ...args: any[]); } export interface ListenerType2 { on(name: string, handler: (event?: any) => void, ...args: any[]); - - off(name: string, handler: (event?: any) => void); + off(name: string, handler: (event?: any) => void, ...args: any[]); } export type UseEventTarget = ListenerType1 | ListenerType2; const defaultTarget = isClient ? window : null; -const useEvent = ( - name: string, - handler?: null | undefined | ((event?: any) => void), - target: null | UseEventTarget = defaultTarget, - options?: any +const isListenerType1 = (target: any): target is ListenerType1 => { + return !!target.addEventListener; +}; +const isListenerType2 = (target: any): target is ListenerType2 => { + return !!target.on; +}; + +type AddEventListener = T extends ListenerType1 ? T['addEventListener'] : T extends ListenerType2 ? T['on'] : never; + +const useEvent = ( + name: Parameters>[0], + handler?: null | undefined | Parameters>[1], + target: null | T | Window = defaultTarget, + options?: Parameters>[2] ) => { useEffect(() => { if (!handler) { @@ -30,11 +37,17 @@ const useEvent = ( if (!target) { return; } - const fn: any = (target as ListenerType1).addEventListener || (target as ListenerType2).on; - fn.call(target, name, handler, options); + if (isListenerType1(target)) { + target.addEventListener(name, handler, options); + } else if (isListenerType2(target)) { + target.on(name, handler, options); + } return () => { - const cleanFn: any = (target as ListenerType1).removeEventListener || (target as ListenerType2).off; - cleanFn.call(target, name, handler, options); + if (isListenerType1(target)) { + target.removeEventListener(name, handler, options); + } else if (isListenerType2(target)) { + target.off(name, handler, options); + } }; }, [name, handler, target, JSON.stringify(options)]); }; diff --git a/src/useFirstMountState.ts b/src/useFirstMountState.ts new file mode 100644 index 0000000000..cf210622a2 --- /dev/null +++ b/src/useFirstMountState.ts @@ -0,0 +1,13 @@ +import { useRef } from 'react'; + +export function useFirstMountState(): boolean { + const isFirst = useRef(true); + + if (isFirst.current) { + isFirst.current = false; + + return true; + } + + return isFirst.current; +} diff --git a/src/useGetSet.ts b/src/useGetSet.ts index 33768aea88..9cb4e02117 100644 --- a/src/useGetSet.ts +++ b/src/useGetSet.ts @@ -1,16 +1,21 @@ -import { useCallback, useRef } from 'react'; +import { Dispatch, useMemo, useRef } from 'react'; import useUpdate from './useUpdate'; +import { HookState, InitialHookState, resolveHookState } from './util/resolveHookState'; -const useGetSet = (initialValue: T): [() => T, (value: T) => void] => { - const state = useRef(initialValue); +export default function useGetSet(initialState: InitialHookState): [() => S, Dispatch>] { + const state = useRef(resolveHookState(initialState)); const update = useUpdate(); - const get = useCallback(() => state.current, []); - const set = useCallback((value: T) => { - state.current = value; - update(); - }, []); - return [get, set]; -}; - -export default useGetSet; + return useMemo( + () => [ + // get + () => state.current as S, + // set + (newState: HookState) => { + state.current = resolveHookState(newState, state.current); + update(); + }, + ], + [] + ); +} diff --git a/src/useList.ts b/src/useList.ts index eecdc5073c..40053c9faf 100644 --- a/src/useList.ts +++ b/src/useList.ts @@ -1,35 +1,154 @@ -import { useState, useMemo } from 'react'; +import { useMemo, useRef } from 'react'; +import useUpdate from './useUpdate'; +import { InitialHookState, ResolvableHookState, resolveHookState } from './util/resolveHookState'; -export interface Actions { - set: (list: T[]) => void; - clear: () => void; +export interface ListActions { + /** + * @description Set new list instead old one + */ + set: (newList: ResolvableHookState) => void; + /** + * @description Add item(s) at the end of list + */ + push: (...items: T[]) => void; + + /** + * @description Replace item at given position. If item at given position not exists it will be set. + */ updateAt: (index: number, item: T) => void; + /** + * @description Insert item at given position, all items to the right will be shifted. + */ + insertAt: (index: number, item: T) => void; + + /** + * @description Replace all items that matches predicate with given one. + */ + update: (predicate: (a: T, b: T) => boolean, newItem: T) => void; + /** + * @description Replace first item matching predicate with given one. + */ + updateFirst: (predicate: (a: T, b: T) => boolean, newItem: T) => void; + /** + * @description Like `updateFirst` bit in case of predicate miss - pushes item to the list + */ + upsert: (predicate: (a: T, b: T) => boolean, newItem: T) => void; + + /** + * @description Sort list with given sorting function + */ + sort: (compareFn?: (a: T, b: T) => number) => void; + /** + * @description Same as native Array's method + */ + filter: (callbackFn: (value: T, index?: number, array?: T[]) => boolean, thisArg?: any) => void; + + /** + * @description Removes item at given position. All items to the right from removed will be shifted. + */ + removeAt: (index: number) => void; + /** + * @deprecated Use removeAt method instead + */ remove: (index: number) => void; - push: (...items: T[]) => void; - filter: (fn: (value: T) => boolean) => void; - sort: (fn?: (a: T, b: T) => number) => void; + + /** + * @description Make the list empty + */ + clear: () => void; + /** + * @description Reset list to initial value + */ reset: () => void; } -const useList = (initialList: T[] = []): [T[], Actions] => { - const [list, set] = useState(initialList); - - const utils = useMemo>( - () => ({ - set, - clear: () => set([]), - updateAt: (index, entry) => - set(currentList => [...currentList.slice(0, index), entry, ...currentList.slice(index + 1)]), - remove: index => set(currentList => [...currentList.slice(0, index), ...currentList.slice(index + 1)]), - push: (...entry) => set(currentList => [...currentList, ...entry]), - filter: fn => set(currentList => currentList.filter(fn)), - sort: (fn?) => set(currentList => [...currentList].sort(fn)), - reset: () => set([...initialList]), - }), - [set] - ); - - return [list, utils]; -}; +function useList(initialList: InitialHookState = []): [T[], ListActions] { + const list = useRef(resolveHookState(initialList)); + const update = useUpdate(); + + const actions = useMemo>(() => { + const a = { + set: (newList: ResolvableHookState) => { + list.current = resolveHookState(newList, list.current); + update(); + }, + + push: (...items: T[]) => { + items.length && actions.set((curr: T[]) => curr.concat(items)); + }, + + updateAt: (index: number, item: T) => { + actions.set((curr: T[]) => { + const arr = curr.slice(); + + arr[index] = item; + + return arr; + }); + }, + + insertAt: (index: number, item: T) => { + actions.set((curr: T[]) => { + const arr = curr.slice(); + + index > arr.length ? (arr[index] = item) : arr.splice(index, 0, item); + + return arr; + }); + }, + + update: (predicate: (a: T, b: T) => boolean, newItem: T) => { + actions.set((curr: T[]) => curr.map(item => (predicate(item, newItem) ? newItem : item))); + }, + + updateFirst: (predicate: (a: T, b: T) => boolean, newItem: T) => { + const index = list.current.findIndex(item => predicate(item, newItem)); + + index >= 0 && actions.updateAt(index, newItem); + }, + + upsert: (predicate: (a: T, b: T) => boolean, newItem: T) => { + const index = list.current.findIndex(item => predicate(item, newItem)); + + index >= 0 ? actions.updateAt(index, newItem) : actions.push(newItem); + }, + + sort: (compareFn?: (a: T, b: T) => number) => { + actions.set((curr: T[]) => curr.slice().sort(compareFn)); + }, + + filter: (callbackFn: (value: T, index: number, array: T[]) => value is S, thisArg?: any) => { + actions.set((curr: T[]) => curr.slice().filter(callbackFn, thisArg)); + }, + + removeAt: (index: number) => { + actions.set((curr: T[]) => { + const arr = curr.slice(); + + arr.splice(index, 1); + + return arr; + }); + }, + + clear: () => { + actions.set([]); + }, + + reset: () => { + actions.set(resolveHookState(initialList).slice()); + }, + }; + + /** + * @deprecated Use removeAt method instead + */ + (a as ListActions).remove = a.removeAt; + + return a as ListActions; + }, []); + + return [list.current, actions]; +} export default useList; diff --git a/src/useLocation.ts b/src/useLocation.ts index 4ab872df1d..0ac4813534 100644 --- a/src/useLocation.ts +++ b/src/useLocation.ts @@ -84,4 +84,6 @@ const useLocationBrowser = (): LocationSensorState => { return state; }; -export default isClient ? useLocationBrowser : useLocationServer; +const hasEventConstructor = typeof Event === 'function'; + +export default isClient && hasEventConstructor ? useLocationBrowser : useLocationServer; diff --git a/src/useLockBodyScroll.ts b/src/useLockBodyScroll.ts index e63bd6cfad..85b81d6346 100644 --- a/src/useLockBodyScroll.ts +++ b/src/useLockBodyScroll.ts @@ -17,7 +17,7 @@ export function getClosestBody(el: Element | HTMLElement | HTMLIFrameElement | n export interface BodyInfoItem { counter: number; - initialOverflow: string | null; + initialOverflow: CSSStyleDeclaration['overflow']; } const bodies: Map = new Map(); diff --git a/src/useMap.ts b/src/useMap.ts index 85a83b5a31..236ac177e6 100644 --- a/src/useMap.ts +++ b/src/useMap.ts @@ -1,18 +1,20 @@ -import { useState, useMemo } from 'react'; +import { useState, useMemo, useCallback } from 'react'; -export interface Actions { - get: (key: K) => T[K]; +export interface StableActions { set: (key: K, value: T[K]) => void; remove: (key: K) => void; reset: () => void; } +export interface Actions extends StableActions { + get: (key: K) => T[K]; +} + const useMap = (initialMap: T = {} as T): [T, Actions] => { const [map, set] = useState(initialMap); - const utils = useMemo>( + const stableActions = useMemo>( () => ({ - get: key => map[key], set: (key, entry) => { set(prevMap => ({ ...prevMap, @@ -30,6 +32,11 @@ const useMap = (initialMap: T = {} as T): [T, Actions [set] ); + const utils = { + get: useCallback(key => map[key], [map]), + ...stableActions, + } as Actions; + return [map, utils]; }; diff --git a/src/useMeasure.ts b/src/useMeasure.ts index 1b61c3cb56..fecd8839c3 100644 --- a/src/useMeasure.ts +++ b/src/useMeasure.ts @@ -1,17 +1,12 @@ import { useCallback, useState } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; -export interface ContentRect { - width: number; - height: number; - top: number; - right: number; - left: number; - bottom: number; -} +export type ContentRect = Pick; const useMeasure = (): [(instance: T) => void, ContentRect] => { - const [rect, set] = useState({ + const [rect, set] = useState({ + x: 0, + y: 0, width: 0, height: 0, top: 0, diff --git a/src/useMediatedState.ts b/src/useMediatedState.ts new file mode 100644 index 0000000000..708b2ecf92 --- /dev/null +++ b/src/useMediatedState.ts @@ -0,0 +1,32 @@ +import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react'; + +export interface StateMediator { + (newState: any): S; + + (newState: any, dispatch: Dispatch>): void; +} + +export type UseMediatedStateReturn = [S, Dispatch>]; + +export function useMediatedState( + mediator: StateMediator +): UseMediatedStateReturn; +export function useMediatedState(mediator: StateMediator, initialState: S): UseMediatedStateReturn; + +export function useMediatedState(mediator: StateMediator, initialState?: S): UseMediatedStateReturn { + const mediatorFn = useRef(mediator); + + const [state, setMediatedState] = useState(initialState!); + const setState = useCallback( + (newState: any) => { + if (mediatorFn.current.length === 2) { + mediatorFn.current(newState, setMediatedState); + } else { + setMediatedState(mediatorFn.current(newState)); + } + }, + [state] + ); + + return [state, setState]; +} diff --git a/src/useMultiStateValidator.ts b/src/useMultiStateValidator.ts new file mode 100644 index 0000000000..1486c5242c --- /dev/null +++ b/src/useMultiStateValidator.ts @@ -0,0 +1,37 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { StateValidator, UseStateValidatorReturn, ValidityState } from './useStateValidator'; + +export type MultiStateValidatorStates = any[] | { [p: string]: any } | { [p: number]: any }; +export type MultiStateValidator = StateValidator; + +export function useMultiStateValidator( + states: S, + validator: MultiStateValidator, + initialValidity: I = [undefined] as I +): UseStateValidatorReturn { + if (typeof states !== 'object') { + throw new Error('states expected to be an object or array, got ' + typeof states); + } + + const validatorInner = useRef(validator); + const statesInner = useRef(states); + + validatorInner.current = validator; + statesInner.current = states; + + const [validity, setValidity] = useState(initialValidity as V); + + const validate = useCallback(() => { + if (validatorInner.current.length >= 2) { + validatorInner.current(statesInner.current, setValidity); + } else { + setValidity(validatorInner.current(statesInner.current)); + } + }, [setValidity]); + + useEffect(() => { + validate(); + }, Object.values(states)); + + return [validity, validate]; +} diff --git a/src/usePreviousDistinct.ts b/src/usePreviousDistinct.ts index c78b00bcd2..896afc73fe 100644 --- a/src/usePreviousDistinct.ts +++ b/src/usePreviousDistinct.ts @@ -1,16 +1,16 @@ import { useRef } from 'react'; +import { useFirstMountState } from './useFirstMountState'; -function strictEquals(prev: T | undefined, next: T) { - return prev === next; -} +export type Predicate = (prev: T | undefined, next: T) => boolean; + +const strictEquals = (prev: T | undefined, next: T) => prev === next; -export default function usePreviousDistinct( - value: T, - compare: (prev: T | undefined, next: T) => boolean = strictEquals -) { +export default function usePreviousDistinct(value: T, compare: Predicate = strictEquals): T | undefined { const prevRef = useRef(); - const curRef = useRef(); - if (!compare(curRef.current, value)) { + const curRef = useRef(value); + const isFirstMount = useFirstMountState(); + + if (!isFirstMount && !compare(curRef.current, value)) { prevRef.current = curRef.current; curRef.current = value; } diff --git a/src/useQueue.ts b/src/useQueue.ts new file mode 100644 index 0000000000..97a8372dce --- /dev/null +++ b/src/useQueue.ts @@ -0,0 +1,37 @@ +import { useState } from 'react'; + +export interface QueueMethods { + add: (item: T) => void; + remove: () => T; + first: T; + last: T; + size: number; +} + +const useQueue = (initialValue: T[] = []): QueueMethods => { + const [state, set] = useState(initialValue); + return { + add: value => { + set(queue => [...queue, value]); + }, + remove: () => { + let result; + set(([first, ...rest]) => { + result = first; + return rest; + }); + return result; + }, + get first() { + return state[0]; + }, + get last() { + return state[state.length - 1]; + }, + get size() { + return state.length; + }, + }; +}; + +export default useQueue; diff --git a/src/useRefMounted.ts b/src/useRefMounted.ts deleted file mode 100644 index 5d285db014..0000000000 --- a/src/useRefMounted.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { RefObject, useEffect, useRef } from 'react'; - -/** - * @deprecated This hook is obsolete, use `useMountedState` instead - */ -const useRefMounted = (): RefObject => { - const refMounted = useRef(false); - - useEffect(() => { - refMounted.current = true; - - return () => { - refMounted.current = false; - }; - }, []); - - return refMounted; -}; - -/** - * @deprecated This hook is obsolete, use `useMountedState` instead - */ -export default useRefMounted; diff --git a/src/useRendersCount.ts b/src/useRendersCount.ts new file mode 100644 index 0000000000..ccb8681b8b --- /dev/null +++ b/src/useRendersCount.ts @@ -0,0 +1,5 @@ +import { useRef } from 'react'; + +export function useRendersCount(): number { + return ++useRef(0).current; +} diff --git a/src/useScrollbarWidth.ts b/src/useScrollbarWidth.ts new file mode 100644 index 0000000000..ff637ce4f9 --- /dev/null +++ b/src/useScrollbarWidth.ts @@ -0,0 +1,21 @@ +import { scrollbarWidth } from '@xobotyi/scrollbar-width'; +import { useEffect, useState } from 'react'; + +export function useScrollbarWidth(): number | undefined { + const [sbw, setSbw] = useState(scrollbarWidth()); + + // this needed to ensure the scrollbar width in case hook called before the DOM is ready + useEffect(() => { + if (typeof sbw !== 'undefined') { + return; + } + + const raf = requestAnimationFrame(() => { + setSbw(scrollbarWidth()); + }); + + return () => cancelAnimationFrame(raf); + }, []); + + return sbw; +} diff --git a/src/useSet.ts b/src/useSet.ts new file mode 100644 index 0000000000..01a9ca2330 --- /dev/null +++ b/src/useSet.ts @@ -0,0 +1,33 @@ +import { useState, useMemo, useCallback } from 'react'; + +export interface StableActions { + add: (key: K) => void; + remove: (key: K) => void; + reset: () => void; +} + +export interface Actions extends StableActions { + has: (key: K) => boolean; +} + +const useSet = (initialSet = new Set()): [Set, Actions] => { + const [set, setSet] = useState(initialSet); + + const stableActions = useMemo>( + () => ({ + add: item => setSet(prevSet => new Set([...Array.from(prevSet), item])), + remove: item => setSet(prevSet => new Set(Array.from(prevSet).filter(i => i !== item))), + reset: () => setSet(initialSet), + }), + [setSet] + ); + + const utils = { + has: useCallback(item => set.has(item), [set]), + ...stableActions, + } as Actions; + + return [set, utils]; +}; + +export default useSet; diff --git a/src/useShallowCompareEffect.ts b/src/useShallowCompareEffect.ts new file mode 100644 index 0000000000..722668a1f9 --- /dev/null +++ b/src/useShallowCompareEffect.ts @@ -0,0 +1,25 @@ +import { DependencyList, EffectCallback } from 'react'; +import { equal as isShallowEqual } from 'fast-shallow-equal'; +import useCustomCompareEffect from './useCustomCompareEffect'; + +const isPrimitive = (val: any) => val !== Object(val); +const shallowEqualDepsList = (prevDeps: DependencyList, nextDeps: DependencyList) => + prevDeps.every((dep, index) => isShallowEqual(dep, nextDeps[index])); + +const useShallowCompareEffect = (effect: EffectCallback, deps: DependencyList) => { + if (process.env.NODE_ENV !== 'production') { + if (!(deps instanceof Array) || !deps.length) { + console.warn('`useShallowCompareEffect` should not be used with no dependencies. Use React.useEffect instead.'); + } + + if (deps.every(isPrimitive)) { + console.warn( + '`useShallowCompareEffect` should not be used with dependencies that are all primitive values. Use React.useEffect instead.' + ); + } + } + + useCustomCompareEffect(effect, deps, shallowEqualDepsList); +}; + +export default useShallowCompareEffect; diff --git a/src/useSize.tsx b/src/useSize.tsx index c0f8776fa0..214ceaf16c 100644 --- a/src/useSize.tsx +++ b/src/useSize.tsx @@ -67,7 +67,7 @@ const useSize = ( } return () => { - if (window) { + if (window && window.removeEventListener) { window.removeEventListener('resize', setSize); } }; diff --git a/src/useStateList.ts b/src/useStateList.ts index 7f4f83b089..a0fc355a2a 100644 --- a/src/useStateList.ts +++ b/src/useStateList.ts @@ -1,33 +1,70 @@ -import { useState, useCallback } from 'react'; - +import { useMemo, useRef } from 'react'; +import useMountedState from './useMountedState'; +import useUpdate from './useUpdate'; import useUpdateEffect from './useUpdateEffect'; -export default function useStateList(stateSet: T[] = []): { state: T; next: () => void; prev: () => void } { - const [currentIndex, setCurrentIndex] = useState(0); +export interface UseStateListReturn { + state: T; + currentIndex: number; + setStateAt: (newIndex: number) => void; + setState: (state: T) => void; + next: () => void; + prev: () => void; +} + +export default function useStateList(stateSet: T[] = []): UseStateListReturn { + const isMounted = useMountedState(); + const update = useUpdate(); + const index = useRef(0); - // In case we receive a different state set, check if the current index still exists and - // reset it to the last if it don't. + // If new state list is shorter that before - switch to the last element useUpdateEffect(() => { - if (!stateSet[currentIndex]) { - setCurrentIndex(stateSet.length - 1); + if (stateSet.length <= index.current) { + index.current = stateSet.length - 1; + update(); } - }, [stateSet]); + }, [stateSet.length]); + + const actions = useMemo( + () => ({ + next: () => actions.setStateAt(index.current + 1), + prev: () => actions.setStateAt(index.current - 1), + setStateAt: (newIndex: number) => { + // do nothing on unmounted component + if (!isMounted()) return; + + // do nothing on empty states list + if (!stateSet.length) return; + + // in case new index is equal current - do nothing + if (newIndex === index.current) return; - const next = useCallback(() => { - const nextStateIndex = stateSet.length === currentIndex + 1 ? 0 : currentIndex + 1; + // it gives the ability to travel through the left and right borders. + // 4ex: if list contains 5 elements, attempt to set index 9 will bring use to 5th element + // in case of negative index it will start counting from the right, so -17 will bring us to 4th element + index.current = newIndex >= 0 ? newIndex % stateSet.length : stateSet.length + (newIndex % stateSet.length); + update(); + }, + setState: (state: T) => { + // do nothing on unmounted component + if (!isMounted()) return; - setCurrentIndex(nextStateIndex); - }, [stateSet, currentIndex]); + const newIndex = stateSet.length ? stateSet.indexOf(state) : -1; - const prev = useCallback(() => { - const prevStateIndex = currentIndex === 0 ? stateSet.length - 1 : currentIndex - 1; + if (newIndex === -1) { + throw new Error(`State '${state}' is not a valid state (does not exist in state list)`); + } - setCurrentIndex(prevStateIndex); - }, [stateSet, currentIndex]); + index.current = newIndex; + update(); + }, + }), + [stateSet] + ); return { - state: stateSet[currentIndex], - next, - prev, + state: stateSet[index.current], + currentIndex: index.current, + ...actions, }; } diff --git a/src/useStateValidator.ts b/src/useStateValidator.ts index cf9070f0a3..f7b897ff1f 100644 --- a/src/useStateValidator.ts +++ b/src/useStateValidator.ts @@ -1,32 +1,35 @@ import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react'; export type ValidityState = [boolean | undefined, ...any[]]; -export type DispatchValidity = Dispatch>; -export type Validator = - | { - (state?: S): V; - (state?: S, dispatch?: DispatchValidity): void; - } - | Function; +export interface StateValidator { + (state: S): V; + + (state: S, dispatch: Dispatch>): void; +} -export type UseValidatorReturn = [V, () => void]; +export type UseStateValidatorReturn = [V, () => void]; -export default function useStateValidator( +export default function useStateValidator( state: S, - validator: Validator, - initialValidity: V = [undefined] as V -): UseValidatorReturn { - const validatorFn = useRef(validator); + validator: StateValidator, + initialState: I = [undefined] as I +): UseStateValidatorReturn { + const validatorInner = useRef(validator); + const stateInner = useRef(state); + + validatorInner.current = validator; + stateInner.current = state; + + const [validity, setValidity] = useState(initialState as V); - const [validity, setValidity] = useState(initialValidity); const validate = useCallback(() => { - if (validatorFn.current.length === 2) { - validatorFn.current(state, setValidity); + if (validatorInner.current.length >= 2) { + validatorInner.current(stateInner.current, setValidity as Dispatch>); } else { - setValidity(validatorFn.current(state)); + setValidity(validatorInner.current(stateInner.current)); } - }, [state]); + }, [setValidity]); useEffect(() => { validate(); diff --git a/src/useStateWithHistory.ts b/src/useStateWithHistory.ts new file mode 100644 index 0000000000..a489293223 --- /dev/null +++ b/src/useStateWithHistory.ts @@ -0,0 +1,131 @@ +import { Dispatch, useCallback, useMemo, useRef, useState } from 'react'; +import { useFirstMountState } from './useFirstMountState'; +import { InitialHookState, ResolvableHookState, resolveHookState } from './util/resolveHookState'; + +interface HistoryState { + history: S[]; + position: number; + capacity: number; + back: (amount?: number) => void; + forward: (amount?: number) => void; + go: (position: number) => void; +} + +export type UseStateHistoryReturn = [S, Dispatch>, HistoryState]; + +export function useStateWithHistory( + initialState: InitialHookState, + capacity?: number, + initialHistory?: I[] +): UseStateHistoryReturn; +export function useStateWithHistory(): UseStateHistoryReturn; + +export function useStateWithHistory( + initialState?: InitialHookState, + capacity: number = 10, + initialHistory?: I[] +): UseStateHistoryReturn { + if (capacity < 1) { + throw new Error(`Capacity has to be greater than 1, got '${capacity}'`); + } + + const isFirstMount = useFirstMountState(); + const [state, innerSetState] = useState(initialState as S); + const history = useRef((initialHistory ?? []) as S[]); + const historyPosition = useRef(0); + + // do the states manipulation only on first mount, no sense to load re-renders with useless calculations + if (isFirstMount) { + if (history.current.length) { + // if last element of history !== initial - push initial to history + if (history.current[history.current.length - 1] !== initialState) { + history.current.push(initialState as I); + } + + // if initial history bigger that capacity - crop the first elements out + if (history.current.length > capacity) { + history.current = history.current.slice(history.current.length - capacity); + } + } else { + // initiate the history with initial state + history.current.push(initialState as I); + } + + historyPosition.current = history.current.length && history.current.length - 1; + } + + const setState = useCallback( + (newState: ResolvableHookState): void => { + innerSetState(currentState => { + newState = resolveHookState(newState); + + // is state has changed + if (newState !== currentState) { + // if current position is not the last - pop element to the right + if (historyPosition.current < history.current.length - 1) { + history.current = history.current.slice(0, historyPosition.current + 1); + } + + historyPosition.current = history.current.push(newState as I) - 1; + + // if capacity is reached - shift first elements + if (history.current.length > capacity) { + history.current = history.current.slice(history.current.length - capacity); + } + } + + return newState; + }); + }, + [state, capacity] + ) as Dispatch>; + + const historyState = useMemo( + () => ({ + history: history.current, + position: historyPosition.current, + capacity, + back: (amount: number = 1) => { + // don't do anything if we already at the left border + if (!historyPosition.current) { + return; + } + + innerSetState(() => { + historyPosition.current -= Math.min(amount, historyPosition.current); + + return history.current[historyPosition.current]; + }); + }, + forward: (amount: number = 1) => { + // don't do anything if we already at the right border + if (historyPosition.current === history.current.length - 1) { + return; + } + + innerSetState(() => { + historyPosition.current = Math.min(historyPosition.current + amount, history.current.length - 1); + + return history.current[historyPosition.current]; + }); + }, + go: (position: number) => { + if (position === historyPosition.current) { + return; + } + + innerSetState(() => { + historyPosition.current = + position < 0 + ? Math.max(history.current.length + position, 0) + : Math.min(history.current.length - 1, position); + + return history.current[historyPosition.current]; + }); + }, + }), + [state] + ); + + return [state, setState, historyState]; +} diff --git a/src/useThrottleFn.ts b/src/useThrottleFn.ts index fafbe7b883..7b197a1f41 100644 --- a/src/useThrottleFn.ts +++ b/src/useThrottleFn.ts @@ -1,19 +1,18 @@ import { useEffect, useRef, useState } from 'react'; import useUnmount from './useUnmount'; -const useThrottleFn = (fn: (...args: any[]) => T, ms: number = 200, args: any[]) => { - const [state, setState] = useState(null as any); +const useThrottleFn = (fn: (...args: U) => T, ms: number = 200, args: U) => { + const [state, setState] = useState(null); const timeout = useRef>(); - const nextArgs = useRef(null) as any; - const hasNextArgs = useRef(false) as any; + const nextArgs = useRef(); useEffect(() => { if (!timeout.current) { setState(fn(...args)); const timeoutCallback = () => { - if (hasNextArgs.current) { - hasNextArgs.current = false; + if (nextArgs.current) { setState(fn(...nextArgs.current)); + nextArgs.current = undefined; timeout.current = setTimeout(timeoutCallback, ms); } else { timeout.current = undefined; @@ -22,7 +21,6 @@ const useThrottleFn = (fn: (...args: any[]) => T, ms: number = 200, args: any timeout.current = setTimeout(timeoutCallback, ms); } else { nextArgs.current = args; - hasNextArgs.current = true; } }, args); diff --git a/src/useTitle.ts b/src/useTitle.ts index 0dbc328664..6d3af7e254 100644 --- a/src/useTitle.ts +++ b/src/useTitle.ts @@ -1,9 +1,11 @@ -import { useEffect } from 'react'; +import { useRef } from 'react'; -const useTitle = (title: string) => { - useEffect(() => { - document.title = title; - }, [title]); -}; +function useTitle(title: string) { + const t = useRef(); -export default useTitle; + if (t.current !== title) { + document.title = t.current = title; + } +} + +export default typeof document !== 'undefined' ? useTitle : (_title: string) => {}; diff --git a/src/useUnmount.ts b/src/useUnmount.ts index c27bd7ea71..2bd460e959 100644 --- a/src/useUnmount.ts +++ b/src/useUnmount.ts @@ -1,7 +1,13 @@ +import { useRef } from 'react'; import useEffectOnce from './useEffectOnce'; -const useUnmount = (fn: () => void | undefined) => { - useEffectOnce(() => fn); +const useUnmount = (fn: () => any): void => { + const fnRef = useRef(fn); + + // update the ref each render so if it change the newest callback will be invoked + fnRef.current = fn; + + useEffectOnce(() => () => fnRef.current()); }; export default useUnmount; diff --git a/src/useUnmountPromise.ts b/src/useUnmountPromise.ts new file mode 100644 index 0000000000..3c3d38010b --- /dev/null +++ b/src/useUnmountPromise.ts @@ -0,0 +1,33 @@ +import { useMemo, useRef, useEffect } from 'react'; + +export type Race =

, E = any>(promise: P, onError?: (error: E) => void) => P; + +const useUnmountPromise = (): Race => { + const refUnmounted = useRef(false); + useEffect(() => () => { + refUnmounted.current = true; + }); + + const wrapper = useMemo(() => { + const race =

, E>(promise: P, onError?: (error: E) => void) => { + const newPromise: P = new Promise((resolve, reject) => { + promise.then( + result => { + if (!refUnmounted.current) resolve(result); + }, + error => { + if (!refUnmounted.current) reject(error); + else if (onError) onError(error); + else console.error('useUnmountPromise', error); + } + ); + }) as P; + return newPromise; + }; + return race; + }, []); + + return wrapper; +}; + +export default useUnmountPromise; diff --git a/src/useUpdate.ts b/src/useUpdate.ts index 495dc04e88..d7f0d36853 100644 --- a/src/useUpdate.ts +++ b/src/useUpdate.ts @@ -1,10 +1,11 @@ -import { useState, useCallback } from 'react'; +import { useCallback, useState } from 'react'; + +const incrementParameter = (num: number): number => ++num % 1_000_000; const useUpdate = () => { const [, setState] = useState(0); // useCallback with empty deps as we only want to define updateCb once - const updateCb = useCallback(() => setState(cnt => cnt + 1), []); - return updateCb; + return useCallback(() => setState(incrementParameter), []); }; export default useUpdate; diff --git a/src/useUpdateEffect.ts b/src/useUpdateEffect.ts index 63fbc80c66..c180d73700 100644 --- a/src/useUpdateEffect.ts +++ b/src/useUpdateEffect.ts @@ -1,14 +1,11 @@ -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; +import { useFirstMountState } from './useFirstMountState'; const useUpdateEffect: typeof useEffect = (effect, deps) => { - const isInitialMount = useRef(true); + const isFirstMount = useFirstMountState(); useEffect(() => { - if (isInitialMount.current) { - isInitialMount.current = false; - } else { - return effect(); - } + !isFirstMount && effect(); }, deps); }; diff --git a/src/useUpsert.ts b/src/useUpsert.ts index b04d552d9a..b12cd3ac44 100644 --- a/src/useUpsert.ts +++ b/src/useUpsert.ts @@ -1,39 +1,26 @@ -import useList, { Actions as ListActions } from './useList'; +import useList, { ListActions } from './useList'; +import { InitialHookState } from './util/resolveHookState'; -export interface Actions extends ListActions { - upsert: (item: T) => void; +export interface UpsertListActions extends Omit, 'upsert'> { + upsert: (newItem: T) => void; } -const useUpsert = ( - comparisonFunction: (upsertedItem: T, existingItem: T) => boolean, - initialList: T[] = [] -): [T[], Actions] => { - const [items, actions] = useList(initialList); - - const upsert = (upsertedItem: T) => { - let itemWasFound = false; - for (let i = 0; i < items.length; i++) { - const existingItem = items[i]; - - const shouldUpdate = comparisonFunction(existingItem, upsertedItem); - if (shouldUpdate) { - actions.updateAt(i, upsertedItem); - itemWasFound = true; - break; - } - } - if (!itemWasFound) { - actions.push(upsertedItem); - } - }; +/** + * @deprecated Use `useList` hook's upsert action instead + */ +export default function useUpsert( + predicate: (a: T, b: T) => boolean, + initialList: InitialHookState = [] +): [T[], UpsertListActions] { + const [list, listActions] = useList(initialList); return [ - items, + list, { - ...actions, - upsert, - }, + ...listActions, + upsert: (newItem: T) => { + listActions.upsert(predicate, newItem); + }, + } as UpsertListActions, ]; -}; - -export default useUpsert; +} diff --git a/src/useVibrate.ts b/src/useVibrate.ts new file mode 100644 index 0000000000..23f7c619a8 --- /dev/null +++ b/src/useVibrate.ts @@ -0,0 +1,37 @@ +import { useEffect } from 'react'; + +export type VibrationPattern = number | number[]; + +const isVibrationApiSupported = typeof navigator === 'object' && 'vibrate' in navigator; + +const useVibrateMock = () => {}; + +function useVibrate(enabled: boolean = true, pattern: VibrationPattern = [1000, 1000], loop: boolean = true): void { + useEffect(() => { + let interval; + + if (enabled) { + navigator.vibrate(pattern); + + if (loop) { + const duration = pattern instanceof Array ? pattern.reduce((a, b) => a + b) : (pattern as number); + + interval = setInterval(() => { + navigator.vibrate(pattern); + }, duration); + } + } + + return () => { + if (enabled) { + navigator.vibrate(0); + + if (loop) { + clearInterval(interval); + } + } + }; + }, [enabled]); +} + +export default isVibrationApiSupported ? useVibrate : useVibrateMock; diff --git a/src/useWait.ts b/src/useWait.ts index cb49802a36..e69de29bb2 100644 --- a/src/useWait.ts +++ b/src/useWait.ts @@ -1,3 +0,0 @@ -import { useWait, Waiter } from 'react-wait'; - -export { useWait, Waiter }; diff --git a/src/useWindowSize.ts b/src/useWindowSize.ts index e3021a08dd..2995796967 100644 --- a/src/useWindowSize.ts +++ b/src/useWindowSize.ts @@ -9,7 +9,7 @@ const useWindowSize = (initialWidth = Infinity, initialHeight = Infinity) => { height: isClient ? window.innerHeight : initialHeight, }); - useEffect(() => { + useEffect((): (() => void) | void => { if (isClient) { const handler = () => { setState({ @@ -23,8 +23,6 @@ const useWindowSize = (initialWidth = Infinity, initialHeight = Infinity) => { return () => { window.removeEventListener('resize', handler); }; - } else { - return undefined; } }, []); diff --git a/src/util/parseTimeRanges.ts b/src/util/parseTimeRanges.ts index e3bfdc3969..1348024ca2 100644 --- a/src/util/parseTimeRanges.ts +++ b/src/util/parseTimeRanges.ts @@ -1,5 +1,5 @@ const parseTimeRanges = ranges => { - const result: Array<{ start: number; end: number }> = []; + const result: { start: number; end: number }[] = []; for (let i = 0; i < ranges.length; i++) { result.push({ diff --git a/src/util/resolveHookState.ts b/src/util/resolveHookState.ts new file mode 100644 index 0000000000..3cb90f5b70 --- /dev/null +++ b/src/util/resolveHookState.ts @@ -0,0 +1,17 @@ +export type StateSetter = (prevState: S) => S; +export type InitialStateSetter = () => S; + +export type InitialHookState = S | InitialStateSetter; +export type HookState = S | StateSetter; +export type ResolvableHookState = S | StateSetter | InitialStateSetter; + +export function resolveHookState(newState: InitialStateSetter): S; +export function resolveHookState(newState: StateSetter, currentState: C): S; +export function resolveHookState(newState: ResolvableHookState, currentState?: C): S; +export function resolveHookState(newState: ResolvableHookState, currentState?: C): S { + if (typeof newState === 'function') { + return (newState as Function)(currentState); + } + + return newState; +} diff --git a/src/comps/__stories__/UseKey.story.tsx b/stories/comps/UseKey.story.tsx similarity index 84% rename from src/comps/__stories__/UseKey.story.tsx rename to stories/comps/UseKey.story.tsx index 71695f6a0e..d4b0d9dd5c 100644 --- a/src/comps/__stories__/UseKey.story.tsx +++ b/stories/comps/UseKey.story.tsx @@ -1,6 +1,6 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import UseKey from '../UseKey'; +import UseKey from '../../src/comps/UseKey'; storiesOf('Components|', module).add('Demo', () => (

diff --git a/stories/createBreakpoint.story.tsx b/stories/createBreakpoint.story.tsx new file mode 100644 index 0000000000..6737ba940a --- /dev/null +++ b/stories/createBreakpoint.story.tsx @@ -0,0 +1,29 @@ +import { withKnobs } from '@storybook/addon-knobs'; +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { createBreakpoint } from '../src'; +import ShowDocs from './util/ShowDocs'; + +const useBreakpointA = createBreakpoint(); +const useBreakpointB = createBreakpoint({ mobileM: 350, laptop: 1024, tablet: 768 }); + +const Demo = () => { + const breakpointA = useBreakpointA(); + const breakpointB = useBreakpointB(); + return ( +
+

{'try resize your window'}

+

{'createBreakpoint() #default : { laptopL: 1440, laptop: 1024, tablet: 768 }'}

+

{breakpointA}

+

{'createBreakpoint({ mobileM: 350, laptop: 1024, tablet: 768 })'}

+

{breakpointB}

+
+ ); +}; + +storiesOf('sensors|createBreakpoint', module) + .addDecorator(withKnobs) + .add('Docs', () => ) + .add('Demo', () => { + return ; + }); diff --git a/src/__stories__/createMemo.story.tsx b/stories/createMemo.story.tsx similarity index 81% rename from src/__stories__/createMemo.story.tsx rename to stories/createMemo.story.tsx index c761749956..51de6712f2 100644 --- a/src/__stories__/createMemo.story.tsx +++ b/stories/createMemo.story.tsx @@ -1,6 +1,6 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { createMemo } from '..'; +import { createMemo } from '../src'; import ShowDocs from './util/ShowDocs'; const fibonacci = n => { @@ -22,5 +22,5 @@ const Demo = () => { }; storiesOf('State|createMemo', module) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/src/__stories__/createReducer.story.tsx b/stories/createReducer.story.tsx similarity index 93% rename from src/__stories__/createReducer.story.tsx rename to stories/createReducer.story.tsx index add19d3903..cf462d4a5d 100644 --- a/src/__stories__/createReducer.story.tsx +++ b/stories/createReducer.story.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import logger from 'redux-logger'; import thunk from 'redux-thunk'; -import { createReducer } from '..'; +import { createReducer } from '../src'; import ShowDocs from './util/ShowDocs'; const useThunkReducer = createReducer(thunk, logger); @@ -53,5 +53,5 @@ const Demo = ({ initialCount = 1 }) => { }; storiesOf('State|createReducer', module) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/src/__stories__/useAsync.story.tsx b/stories/useAsync.story.tsx similarity index 90% rename from src/__stories__/useAsync.story.tsx rename to stories/useAsync.story.tsx index cc00e21832..e1ffd21519 100644 --- a/src/__stories__/useAsync.story.tsx +++ b/stories/useAsync.story.tsx @@ -1,7 +1,7 @@ import { number, withKnobs } from '@storybook/addon-knobs'; import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useAsync } from '..'; +import { useAsync } from '../src'; import ShowDocs from './util/ShowDocs'; const Demo = ({ delay }) => { @@ -35,7 +35,7 @@ const Demo = ({ delay }) => { storiesOf('Side effects|useAsync', module) .addDecorator(withKnobs) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => { const delay = number('delay', 1000, { range: true, min: 100, max: 5000, step: 100 }); return ; diff --git a/src/__stories__/useAsyncFn.story.tsx b/stories/useAsyncFn.story.tsx similarity index 88% rename from src/__stories__/useAsyncFn.story.tsx rename to stories/useAsyncFn.story.tsx index eb630855ba..81b1af4923 100644 --- a/src/__stories__/useAsyncFn.story.tsx +++ b/stories/useAsyncFn.story.tsx @@ -1,6 +1,6 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useAsyncFn } from '..'; +import { useAsyncFn } from '../src'; import ShowDocs from './util/ShowDocs'; const Demo = () => { @@ -33,5 +33,5 @@ const Demo = () => { }; storiesOf('Side effects|useAsyncFn', module) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/src/__stories__/useAsyncRetry.story.tsx b/stories/useAsyncRetry.story.tsx similarity index 90% rename from src/__stories__/useAsyncRetry.story.tsx rename to stories/useAsyncRetry.story.tsx index d17b1b5904..be54b8ced3 100644 --- a/src/__stories__/useAsyncRetry.story.tsx +++ b/stories/useAsyncRetry.story.tsx @@ -1,7 +1,7 @@ import { number, withKnobs } from '@storybook/addon-knobs'; import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useAsyncRetry } from '..'; +import { useAsyncRetry } from '../src'; import ShowDocs from './util/ShowDocs'; const Demo = ({ delay }) => { @@ -36,7 +36,7 @@ const Demo = ({ delay }) => { storiesOf('Side effects|useAsyncRetry', module) .addDecorator(withKnobs) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => { const delay = number('delay', 1000, { range: true, min: 100, max: 5000, step: 100 }); return ; diff --git a/src/__stories__/useAudio.story.tsx b/stories/useAudio.story.tsx similarity index 91% rename from src/__stories__/useAudio.story.tsx rename to stories/useAudio.story.tsx index 24431ceec2..c226668405 100644 --- a/src/__stories__/useAudio.story.tsx +++ b/stories/useAudio.story.tsx @@ -1,6 +1,6 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useAudio } from '..'; +import { useAudio } from '../src'; import ShowDocs from './util/ShowDocs'; const Demo = () => { @@ -30,5 +30,5 @@ const Demo = () => { }; storiesOf('UI|useAudio', module) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/src/__stories__/useBattery.story.tsx b/stories/useBattery.story.tsx similarity index 91% rename from src/__stories__/useBattery.story.tsx rename to stories/useBattery.story.tsx index 9a38321a3a..79f9c16dba 100644 --- a/src/__stories__/useBattery.story.tsx +++ b/stories/useBattery.story.tsx @@ -1,6 +1,6 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useBattery } from '..'; +import { useBattery } from '../src'; import ShowDocs from './util/ShowDocs'; const Demo = () => { @@ -37,5 +37,5 @@ const Demo = () => { }; storiesOf('Sensors|useBattery', module) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/src/__stories__/useBeforeUnload.story.tsx b/stories/useBeforeUnload.story.tsx similarity index 79% rename from src/__stories__/useBeforeUnload.story.tsx rename to stories/useBeforeUnload.story.tsx index eb4ea243fe..d87a14bd96 100644 --- a/src/__stories__/useBeforeUnload.story.tsx +++ b/stories/useBeforeUnload.story.tsx @@ -1,6 +1,6 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useBeforeUnload, useToggle } from '..'; +import { useBeforeUnload, useToggle } from '../src'; import ShowDocs from './util/ShowDocs'; const Demo = () => { @@ -16,5 +16,5 @@ const Demo = () => { }; storiesOf('Side effects|useBeforeUnload', module) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/src/__stories__/useBoolean.story.tsx b/stories/useBoolean.story.tsx similarity index 82% rename from src/__stories__/useBoolean.story.tsx rename to stories/useBoolean.story.tsx index 472682f13a..66c28020bb 100644 --- a/src/__stories__/useBoolean.story.tsx +++ b/stories/useBoolean.story.tsx @@ -1,6 +1,6 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useBoolean } from '..'; +import { useBoolean } from '../src'; import ShowDocs from './util/ShowDocs'; const Demo = () => { @@ -17,5 +17,5 @@ const Demo = () => { }; storiesOf('State|useBoolean', module) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/src/__stories__/useClickAway.story.tsx b/stories/useClickAway.story.tsx similarity index 81% rename from src/__stories__/useClickAway.story.tsx rename to stories/useClickAway.story.tsx index 67c8c75a36..e5ab7d951c 100644 --- a/src/__stories__/useClickAway.story.tsx +++ b/stories/useClickAway.story.tsx @@ -2,7 +2,7 @@ import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; import * as React from 'react'; import { useRef } from 'react'; -import { useClickAway } from '..'; +import { useClickAway } from '../src'; import ShowDocs from './util/ShowDocs'; const Demo = () => { @@ -22,5 +22,5 @@ const Demo = () => { }; storiesOf('UI|useClickAway', module) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/src/__stories__/useCopyToClipboard.story.tsx b/stories/useCopyToClipboard.story.tsx similarity index 87% rename from src/__stories__/useCopyToClipboard.story.tsx rename to stories/useCopyToClipboard.story.tsx index 82fba372f1..af9a1b2cb4 100644 --- a/src/__stories__/useCopyToClipboard.story.tsx +++ b/stories/useCopyToClipboard.story.tsx @@ -1,6 +1,6 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useCopyToClipboard } from '..'; +import { useCopyToClipboard } from '../src'; import ShowDocs from './util/ShowDocs'; const Demo = () => { @@ -30,5 +30,5 @@ const Demo = () => { }; storiesOf('Side-effects|useCopyToClipboard', module) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/src/__stories__/useCounter.story.tsx b/stories/useCounter.story.tsx similarity index 72% rename from src/__stories__/useCounter.story.tsx rename to stories/useCounter.story.tsx index 8c64f8db20..dba74c5211 100644 --- a/src/__stories__/useCounter.story.tsx +++ b/stories/useCounter.story.tsx @@ -1,12 +1,13 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useCounter } from '..'; +import { useCounter } from '../src'; import ShowDocs from './util/ShowDocs'; const Demo = () => { + const [initialValue, setInitialValue] = React.useState(5); const [min, { inc: incMin, dec: decMin }] = useCounter(1); const [max, { inc: incMax, dec: decMax }] = useCounter(10); - const [value, { inc, dec, set, reset }] = useCounter(5, max, min); + const [value, { inc, dec, set, reset }] = useCounter(initialValue, max, min); return (
@@ -31,10 +32,15 @@ const Demo = () => { Max value: +
+
+ Initial value: {initialValue} + +
); }; storiesOf('State|useCounter', module) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/src/__stories__/useCss.story.tsx b/stories/useCss.story.tsx similarity index 79% rename from src/__stories__/useCss.story.tsx rename to stories/useCss.story.tsx index a1099abba4..1279954cff 100644 --- a/src/__stories__/useCss.story.tsx +++ b/stories/useCss.story.tsx @@ -1,6 +1,6 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useCss } from '..'; +import { useCss } from '../src'; import ShowDocs from './util/ShowDocs'; const Demo = () => { @@ -16,5 +16,5 @@ const Demo = () => { }; storiesOf('UI|useCss', module) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/stories/useCustomCompareEffect.story.tsx b/stories/useCustomCompareEffect.story.tsx new file mode 100644 index 0000000000..98f470c76d --- /dev/null +++ b/stories/useCustomCompareEffect.story.tsx @@ -0,0 +1,38 @@ +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; +import { useCounter, useCustomCompareEffect } from '../src'; +import isDeepEqual from 'react-fast-compare'; +import ShowDocs from './util/ShowDocs'; + +const Demo = () => { + const [countNormal, { inc: incNormal }] = useCounter(0); + const [countDeep, { inc: incDeep }] = useCounter(0); + const options = { max: 500 }; + + React.useEffect(() => { + if (countNormal < options.max) { + incNormal(); + } + }, [options]); + + useCustomCompareEffect( + () => { + if (countNormal < options.max) { + incDeep(); + } + }, + [options], + (prevDeps, nextDeps) => isDeepEqual(prevDeps, nextDeps) + ); + + return ( +
+

useEffect: {countNormal}

+

useCustomCompareEffect: {countDeep}

+
+ ); +}; + +storiesOf('Lifecycle|useCustomCompareEffect', module) + .add('Docs', () => ) + .add('Demo', () => ); diff --git a/src/__stories__/useDebounce.story.tsx b/stories/useDebounce.story.tsx similarity index 74% rename from src/__stories__/useDebounce.story.tsx rename to stories/useDebounce.story.tsx index 788f33e07d..cc588b9183 100644 --- a/src/__stories__/useDebounce.story.tsx +++ b/stories/useDebounce.story.tsx @@ -1,6 +1,6 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useDebounce } from '..'; +import { useDebounce } from '../src'; import ShowDocs from './util/ShowDocs'; const Demo = () => { @@ -8,7 +8,7 @@ const Demo = () => { const [val, setVal] = React.useState(''); const [debouncedValue, setDebouncedValue] = React.useState(''); - useDebounce( + const [, cancel] = useDebounce( () => { setState('Typing stopped'); setDebouncedValue(val); @@ -29,11 +29,14 @@ const Demo = () => { }} />
{state}
-
Debounced value: {debouncedValue}
+
+ Debounced value: {debouncedValue} + +
); }; storiesOf('Side effects|useDebounce', module) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/src/__stories__/useDeepCompareEffect.story.tsx b/stories/useDeepCompareEffect.story.tsx similarity index 83% rename from src/__stories__/useDeepCompareEffect.story.tsx rename to stories/useDeepCompareEffect.story.tsx index 3f06d7d917..bdad410cf4 100644 --- a/src/__stories__/useDeepCompareEffect.story.tsx +++ b/stories/useDeepCompareEffect.story.tsx @@ -1,6 +1,6 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useCounter, useDeepCompareEffect } from '..'; +import { useCounter, useDeepCompareEffect } from '../src'; import ShowDocs from './util/ShowDocs'; const Demo = () => { @@ -29,5 +29,5 @@ const Demo = () => { }; storiesOf('Lifecycle|useDeepCompareEffect', module) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/src/__stories__/useDefault.story.tsx b/stories/useDefault.story.tsx similarity index 83% rename from src/__stories__/useDefault.story.tsx rename to stories/useDefault.story.tsx index 3cf0bfef94..95d4dbe1a9 100644 --- a/src/__stories__/useDefault.story.tsx +++ b/stories/useDefault.story.tsx @@ -1,6 +1,6 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useDefault } from '..'; +import { useDefault } from '../src'; import ShowDocs from './util/ShowDocs'; const Demo = () => { @@ -18,5 +18,5 @@ const Demo = () => { }; storiesOf('State|useDefault', module) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/src/__stories__/useDrop.story.tsx b/stories/useDrop.story.tsx similarity index 92% rename from src/__stories__/useDrop.story.tsx rename to stories/useDrop.story.tsx index 51fe48dc9b..b9443a1cfb 100644 --- a/src/__stories__/useDrop.story.tsx +++ b/stories/useDrop.story.tsx @@ -1,7 +1,7 @@ import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useDrop } from '..'; +import { useDrop } from '../src'; import ShowDocs from './util/ShowDocs'; const Demo = () => { @@ -50,5 +50,5 @@ const Demo = () => { }; storiesOf('UI|useDrop', module) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/src/__stories__/useDropArea.story.tsx b/stories/useDropArea.story.tsx similarity index 92% rename from src/__stories__/useDropArea.story.tsx rename to stories/useDropArea.story.tsx index 277d98711b..a99650da3a 100644 --- a/src/__stories__/useDropArea.story.tsx +++ b/stories/useDropArea.story.tsx @@ -1,7 +1,7 @@ import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useDropArea } from '..'; +import { useDropArea } from '../src'; import ShowDocs from './util/ShowDocs'; const Demo = () => { @@ -52,5 +52,5 @@ const Demo = () => { }; storiesOf('UI|useDropArea', module) - .add('Docs', () => ) + .add('Docs', () => ) .add('Default', () => ); diff --git a/src/__stories__/useEffectOnce.story.tsx b/stories/useEffectOnce.story.tsx similarity index 79% rename from src/__stories__/useEffectOnce.story.tsx rename to stories/useEffectOnce.story.tsx index 17dd4fd81c..6546d62736 100644 --- a/src/__stories__/useEffectOnce.story.tsx +++ b/stories/useEffectOnce.story.tsx @@ -1,6 +1,6 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useEffectOnce } from '..'; +import { useEffectOnce } from '../src'; import ConsoleStory from './util/ConsoleStory'; import ShowDocs from './util/ShowDocs'; @@ -17,5 +17,5 @@ const Demo = () => { }; storiesOf('Lifecycle|useEffectOnce', module) - .add('Docs', () => ) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/stories/useEnsuredForwardedRef.story.tsx b/stories/useEnsuredForwardedRef.story.tsx new file mode 100644 index 0000000000..2bd661f613 --- /dev/null +++ b/stories/useEnsuredForwardedRef.story.tsx @@ -0,0 +1,79 @@ +import { storiesOf } from '@storybook/react'; +import React, { forwardRef, useRef, useState, useEffect, MutableRefObject } from 'react'; +import { useEnsuredForwardedRef } from '../src'; +import ShowDocs from './util/ShowDocs'; + +import { boolean, withKnobs } from '@storybook/addon-knobs'; + +const INITIAL_SIZE = { + width: null, + height: null, +}; + +const Demo = ({ activeForwardRef }) => { + const ref = useRef(null); + + const [size, setSize] = useState(INITIAL_SIZE); + + useEffect(() => { + handleClick(); + }, [activeForwardRef]); + + const handleClick = () => { + if (activeForwardRef) { + const { width, height } = ref.current.getBoundingClientRect(); + setSize({ + width, + height, + }); + } else { + setSize(INITIAL_SIZE); + } + }; + + return ( + <> + +
Parent component using external ref: (textarea size)
+
{JSON.stringify(size, null, 2)}
+ + + ); +}; + +const Child = forwardRef(({}, ref: MutableRefObject) => { + const ensuredForwardRef = useEnsuredForwardedRef(ref); + + const [size, setSize] = useState(INITIAL_SIZE); + + useEffect(() => { + handleMouseUp(); + }, []); + + const handleMouseUp = () => { + const { width, height } = ensuredForwardRef.current.getBoundingClientRect(); + setSize({ + width, + height, + }); + }; + + return ( + <> +
Child forwardRef component using forwardRef: (textarea size)
+
{JSON.stringify(size, null, 2)}
+
You can resize this textarea:
+