├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── tests_checker.yml └── workflows │ ├── branch-tests.yml │ └── ci.yml ├── .gitignore ├── .huskyrc ├── .mocharc.json ├── .npmrc ├── .nycrc ├── .releaserc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── HOOK_DOCUMENTATION_TEMPLATE.md ├── LICENSE.txt ├── README.md ├── babel.config.js ├── docs ├── Installation.md ├── Introduction.md ├── README.es-ES.md ├── README.it-IT.md ├── README.jp-JP.md ├── README.pl-PL.md ├── README.pt-BR.md ├── README.tr-TR.md ├── README.uk-UA.md ├── README.zh-CN.md ├── useAudio.md ├── useConditionalTimeout.md ├── useCookie.md ├── useDarkMode.md ├── useDebouncedCallback.md ├── useDefaultedState.md ├── useDidMount.md ├── useDrag.md ├── useDragEvents.md ├── useDropZone.md ├── useEvent.md ├── useGeolocation.md ├── useGeolocationEvents.md ├── useGeolocationState.md ├── useGlobalEvent.md ├── useHorizontalSwipe.md ├── useInfiniteScroll.md ├── useInterval.md ├── useIsFirstRender.md ├── useLifecycle.md ├── useLocalStorage.md ├── useLongPress.md ├── useMediaQuery.md ├── useMouse.md ├── useMouseEvents.md ├── useMouseState.md ├── useMutableState.md ├── useMutationObserver.md ├── useObjectState.md ├── useObservable.md ├── useOnlineState.md ├── usePreviousValue.md ├── useQueryParam.md ├── useQueryParams.md ├── useRenderInfo.md ├── useRequestAnimationFrame.md ├── useResizeObserver.md ├── useSearchQuery.md ├── useSessionStorage.md ├── useSpeechRecognition.md ├── useSpeechSynthesis.md ├── useSwipe.md ├── useSwipeEvents.md ├── useSystemVoices.md ├── useThrottledCallback.md ├── useTimeout.md ├── useToggle.md ├── useTouch.md ├── useTouchEvents.md ├── useTouchState.md ├── useURLSearchParams.md ├── useUnmount.md ├── useUpdateEffect.md ├── useValidatedState.md ├── useValueHistory.md ├── useVerticalSwipe.md ├── useViewportSpy.md ├── useViewportState.md ├── useWillUnmount.md ├── useWindowResize.md ├── useWindowScroll.md └── utils │ ├── _CustomLogo.js │ ├── _EmptyComponent.js │ ├── _custom.css │ ├── _doc-logo.png │ ├── _setup.js │ └── _styleguidist.theme.js ├── logo.png ├── package.json ├── scripts ├── commit-version.sh ├── generate-doc-append-types.js ├── generate-exports.js └── update-version.js ├── src ├── factory │ ├── createHandlerSetter.ts │ └── createStorageHook.ts ├── shared │ ├── geolocationUtils.ts │ ├── isAPISupported.ts │ ├── isClient.ts │ ├── isDevelopment.ts │ ├── isFunction.ts │ ├── noop.ts │ ├── safeHasOwnProperty.ts │ ├── safelyParseJson.ts │ ├── swipeUtils.ts │ ├── types.ts │ └── warnOnce.ts ├── useAudio.ts ├── useConditionalTimeout.ts ├── useCookie.ts ├── useDarkMode.ts ├── useDebouncedCallback.ts ├── useDefaultedState.ts ├── useDidMount.ts ├── useDrag.ts ├── useDragEvents.ts ├── useDropZone.ts ├── useEvent.ts ├── useGeolocation.ts ├── useGeolocationEvents.ts ├── useGeolocationState.ts ├── useGlobalEvent.ts ├── useHorizontalSwipe.ts ├── useInfiniteScroll.ts ├── useInterval.ts ├── useIsFirstRender.ts ├── useLifecycle.ts ├── useLocalStorage.ts ├── useLongPress.ts ├── useMediaQuery.ts ├── useMouse.ts ├── useMouseEvents.ts ├── useMouseState.ts ├── useMutableState.ts ├── useMutationObserver.ts ├── useObjectState.ts ├── useObservable.ts ├── useOnlineState.ts ├── usePreviousValue.ts ├── useQueryParam.ts ├── useQueryParams.ts ├── useRenderInfo.ts ├── useRequestAnimationFrame.ts ├── useResizeObserver.ts ├── useSearchQuery.ts ├── useSessionStorage.ts ├── useSpeechRecognition.ts ├── useSpeechSynthesis.ts ├── useSwipe.ts ├── useSwipeEvents.ts ├── useSystemVoices.ts ├── useThrottledCallback.ts ├── useTimeout.ts ├── useToggle.ts ├── useTouch.ts ├── useTouchEvents.ts ├── useTouchState.ts ├── useURLSearchParams.ts ├── useUnmount.ts ├── useUpdateEffect.ts ├── useValidatedState.ts ├── useValueHistory.ts ├── useVerticalSwipe.ts ├── useViewportSpy.ts ├── useViewportState.ts ├── useWillUnmount.ts ├── useWindowResize.ts └── useWindowScroll.ts ├── styleguide.config.js ├── test ├── _setup.js ├── geolocationUtils.spec.js ├── isAPISupported.spec.js ├── isClient.spec.js ├── mocks │ ├── AudioApi.mock.js │ ├── CookieStoreApi.mock.js │ ├── GeoLocationApi.mock.js │ ├── IntersectionObserver.mock.js │ ├── MatchMediaQueryList.mock.js │ ├── ResizeObserver.mock.js │ ├── SpeechSynthesis.mock.js │ └── SpeechSynthesisUtterance.mock.js ├── safeHasOwnProperty.spec.js ├── useAudio.spec.js ├── useConditionalTimeout.spec.js ├── useCookie.spec.js ├── useDarkMode.spec.js ├── useDebouncedCallback.spec.js ├── useDefaultedState.spec.js ├── useDidMount.spec.js ├── useDrag.spec.js ├── useDragEvents.spec.js ├── useDropZone.spec.js ├── useEvent.spec.js ├── useGeolocation.spec.js ├── useGeolocationEvents.spec.js ├── useGeolocationState.spec.js ├── useGlobalEvent.spec.js ├── useHandlerSetter.spec.js ├── useInfiniteScroll.spec.js ├── useInterval.spec.js ├── useIsFirstRender.spec.js ├── useLifecycle.spec.js ├── useLocalStorage.spec.js ├── useLongPress.spec.js ├── useMediaQuery.spec.js ├── useMouse.spec.js ├── useMouseEvents.spec.js ├── useMouseState.spec.js ├── useMutableState.spec.js ├── useMutationObserver.spec.js ├── useObjectState.spec.js ├── useObservable.spec.js ├── useOnlineState.spec.js ├── usePreviousValue.spec.js ├── useQueryParam.spec.js ├── useQueryParams.spec.js ├── useRenderInfo.spec.js ├── useRequestAnimationFrame.spec.js ├── useResizeObserver.spec.js ├── useSearchQuery.spec.js ├── useSessionStorage.spec.js ├── useSpeechRecognition.spec.js ├── useSpeechSynthesis.spec.js ├── useStorage.spec.js ├── useSwipe.spec.js ├── useSwipeEvents.spec.js ├── useSystemVoices.spec.js ├── useThrottledCallback.spec.js ├── useTimeout.spec.js ├── useToggle.spec.js ├── useTouchEvents.spec.js ├── useTouchState.spec.js ├── useURLSearchParams.spec.js ├── useUnmount.spec.js ├── useUpdateEffect.spec.js ├── useValidatedState.spec.js ├── useValueHistory.spec.js ├── useViewportSpy.spec.js ├── useViewportState.spec.js ├── useWillUnmount.spec.js ├── useWindowResize.spec.js ├── useWindowScroll.spec.js ├── utils │ ├── ReactRouterWrapper.js │ ├── assertFunction.js │ ├── assertHook.js │ └── promiseDelay.js └── warnOnce.spec.js ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── tsconfig.json ├── tsconfig.types.json └── usage_example.png /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /docs 3 | /test 4 | /docs 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true 5 | }, 6 | extends: [ 7 | 'plugin:react/recommended', 8 | 'standard-with-typescript' 9 | ], 10 | parserOptions: { 11 | project: './tsconfig.json', 12 | ecmaVersion: 'latest', 13 | sourceType: 'module' 14 | }, 15 | plugins: [ 16 | 'react' 17 | ], 18 | rules: { 19 | 'max-len': [ 20 | 'error', 21 | { 22 | code: 140 23 | } 24 | ], 25 | semi: [ 26 | 2, 27 | 'never' 28 | ], 29 | '@typescript-eslint/semi': 'off', 30 | 'linebreak-style': 'off', 31 | 'object-curly-newline': 'off', 32 | 'react/jsx-filename-extension': 'off', 33 | 'import/no-named-as-default': 'off', 34 | 'import/no-named-as-default-member': 'off', 35 | '@typescript-eslint/explicit-function-return-type': 'off', 36 | '@typescript-eslint/strict-boolean-expressions': 'off', 37 | '@typescript-eslint/no-non-null-assertion': 'off', 38 | '@typescript-eslint/no-invalid-void-type': 'off' 39 | }, 40 | overrides: [ 41 | { 42 | files: [ 43 | '*.test.js', 44 | '*.spec.js', 45 | '*.test.jsx', 46 | '*.spec.jsx' 47 | ], 48 | globals: { 49 | expect: 'readonly', 50 | should: 'readonly', 51 | sinon: 'readonly' 52 | }, 53 | rules: { 54 | 'no-unused-expressions': 'off' 55 | } 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Related Issue 7 | 8 | 9 | 10 | 11 | 12 | ## Motivation and Context 13 | 14 | 15 | 16 | ## How Has This Been Tested? 17 | 18 | 19 | 20 | 21 | ## Screenshots (if appropriate): 22 | -------------------------------------------------------------------------------- /.github/tests_checker.yml: -------------------------------------------------------------------------------- 1 | comment: 'Hello, thank you for contributing! It looks like your PR introduces new code that has not been tested, please make to add some tests as soon as you can.', 2 | fileExtensions: ['.ts', '.js'] 3 | testDir: 'test' 4 | -------------------------------------------------------------------------------- /.github/workflows/branch-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - master 7 | pull_request: 8 | branches-ignore: 9 | - master 10 | 11 | jobs: 12 | test: 13 | if: "!contains(github.event.head_commit.message, 'skip ci')" 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: 18.14 21 | 22 | - run: npm install 23 | - run: npm run lint 24 | - run: npm run build 25 | - run: npm test 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | env: 4 | CI: true 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | jobs: 13 | ci-cd: 14 | if: "!contains(github.event.head_commit.message, 'skip ci')" 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: 18.14 21 | registry-url: https://registry.npmjs.org/ 22 | 23 | - name: NPM Install 24 | run: npm install 25 | 26 | - name: Build 27 | run: npm run build 28 | 29 | - name: Tests (with coverage) 30 | run: npm test -- --coverage 31 | 32 | - name: Coveralls GitHub Action 33 | uses: coverallsapp/github-action@v2 34 | with: 35 | github-token: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Build website (Github pages) 38 | run: npm run build-doc --if-present 39 | 40 | - name: Publish website on GitHub Pages 41 | uses: crazy-max/ghaction-github-pages@v3 42 | with: 43 | build_dir: dist-ghpages 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | 47 | - name: Prepare distribution 48 | run: | 49 | node scripts/generate-exports.js 50 | cp package.json README.md LICENSE.txt CHANGELOG.md CONTRIBUTING.md CODE_OF_CONDUCT.md ./dist 51 | 52 | - name: Publish 53 | run: | 54 | npm pack 55 | npx semantic-release 56 | working-directory: ./dist 57 | env: 58 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "npm run lint" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": [ 3 | "jsdom-global/register", 4 | "@babel/register", 5 | "regenerator-runtime/runtime", 6 | "mock-local-storage", 7 | "./test/_setup.js" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "reporter": ["lcov"], 4 | "check-coverage": true, 5 | "extension": [ ".js" ], 6 | "include": [ "dist/*.js" ], 7 | "exclude": [ "dist/index.js", "dist/_virtual/**/*.js" ], 8 | "branches": 50, 9 | "lines": 80, 10 | "functions": 70, 11 | "statements": 70 12 | } 13 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "master" 4 | ], 5 | "tagFormat": "v${version}", 6 | "plugins": [ 7 | "@semantic-release/commit-analyzer", 8 | [ 9 | "@semantic-release/exec", 10 | { 11 | "successCmd": "node ../scripts/update-version.js ${nextRelease.version} && node ../scripts/generate-exports.js && sh ../scripts/commit-version.sh ${nextRelease.version}" 12 | } 13 | ], 14 | "@semantic-release/npm", 15 | "@semantic-release/changelog", 16 | [ 17 | "@semantic-release/git", 18 | { 19 | "assets": [ 20 | "../package.json" 21 | ], 22 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 23 | } 24 | ], 25 | "@semantic-release/github" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /HOOK_DOCUMENTATION_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # useYourHookName 2 | 3 | A hook that [...] 4 | 5 | ### 💡 Why? 6 | 7 | - why this hook is necessary and what it does 8 | 9 | ### Basic Usage: 10 | 11 | ```jsx harmony 12 | import { yourHook } from 'beautiful-react-hooks'; 13 | 14 | const YourExample = () => { 15 | /* Your code goes here */ 16 | 17 | return null; 18 | }; 19 | 20 | 21 | ``` 22 | 23 | ### Use cases 24 | 25 | description of the use case 26 | 27 | ```jsx harmony 28 | import { yourHook } from 'beautiful-react-hooks'; 29 | 30 | const YourUseCase = () => { 31 | /* Your code goes here */ 32 | 33 | return null; 34 | }; 35 | 36 | 37 | ``` 38 | 39 | ### Mastering the hooks 40 | 41 | #### ✅ When to use 42 | 43 | - When it's good to use 44 | 45 | #### 🛑 When not to use 46 | 47 | - When it's not good to use 48 | 49 | 50 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Antonio Russo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/react', '@babel/env'] 3 | } 4 | -------------------------------------------------------------------------------- /docs/Installation.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | Using `npm`: 4 | 5 | ```bash 6 | $ npm i --save beautiful-react-hooks 7 | ``` 8 | 9 | Using `yarn`: 10 | 11 | ```bash 12 | $ yarn add beautiful-react-hooks 13 | ``` 14 | 15 | then just import any hook described by the documentation in your React component file: 16 | 17 | ```ts static 18 | import useSomeHook from 'beautiful-react-hoks/useSomeHook' 19 | ``` 20 | 21 | **Please note**: always import your hook from the library as a single module to avoid importing unnecessary hooks and therefore unnecessary 22 | dependencies 23 | 24 | ## Peer dependencies 25 | 26 | Some hooks are built on top of third-party libraries (rxjs, react-router-dom, redux), therefore you will notice those libraries listed as 27 | peer dependencies. You don't have to install these dependencies unless you directly use those hooks. 28 | 29 | ## Working with Refs in TypeScript 30 | 31 | The documentation of this module is written in JavaScript, so you will see a lot of this: 32 | 33 | ```jsx static 34 | import { ref } from 'react'; 35 | 36 | const myCustomHook = () => { 37 | const ref = useRef() 38 | 39 | /* your code */ 40 | 41 | return ref; 42 | } 43 | ``` 44 | 45 | If you are in a TypeScript project, you should declare your ref as a `RefObject`. For example: 46 | 47 | ```ts static 48 | import { ref } from 'react'; 49 | 50 | const myCustomHook = () => { 51 | const ref = useRef(null); 52 | 53 | /* your code */ 54 | 55 | return ref; 56 | } 57 | ``` 58 | 59 | See [here](https://dev.to/wojciechmatuszewski/mutable-and-immutable-useref-semantics-with-react-typescript-30c9) for information on the 60 | difference between a `MutableRefObject` and a `RefObject`. 61 | -------------------------------------------------------------------------------- /docs/Introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | [](https://travis-ci.org/beautifulinteractions/beautiful-react-hooks) 4 | [](https://opensource.org/licenses/MIT) 5 |  6 |  7 | 8 | `beautiful-react-hooks` is a collection of tailor-made [React hooks](https://beta.reactjs.org/reference/react) to enhance your development 9 | process and make it faster. 10 | 11 | ## 💡 Why? 12 | 13 | Custom React hooks allow developers to abstract the business logic of components into single, reusable functions. 14 | I have noticed that many of the hooks I have created and shared across projects involve callbacks, references, events, and dealing with the 15 | component lifecycle. 16 | Therefore, I have created `beautiful-react-hooks`, a collection of useful [React hooks](https://beta.reactjs.org/reference/react) that may 17 | help other developers speed up their development process. 18 | Moreover, I have strived to create a concise and practical API that emphasizes code readability, while keeping the learning curve as low as 19 | possible, making it suitable for larger teams to use and share 20 | 21 | ## ☕️ Features 22 | 23 | * Concise API 24 | * Small and lightweight 25 | * Easy to learn 26 | 27 | ## Basic usage 28 | 29 | importing a hooks is as easy as the following straightforward line: 30 | 31 | ```ts static 32 | import useSomeHook from 'beautiful-react-hooks/useSomeHook' 33 | ``` 34 | 35 | ## Peer dependencies 36 | 37 | Some hooks are built using third-party libraries (such as rxjs, react-router-dom, redux). As a result, you will see these libraries listed 38 | as peer dependencies.\ 39 | Unless you are using these hooks directly, you need not install these dependencies. 40 | -------------------------------------------------------------------------------- /docs/useDarkMode.md: -------------------------------------------------------------------------------- 1 | # useDarkMode 2 | 3 | A hook that manages all the necessary operations to incorporate a toggle switch for dark and light modes on your website 4 | 5 | ### 💡 Why? 6 | 7 | - Keep information about dark/light mode consistent and in sync across sessions using localStorage 8 | - Return the methods that allows you to change into dark/light mode 9 | - Safely read information about the dark/light mode from user's operating system using `prefers-color-scheme` 10 | 11 | ### Basic Usage: 12 | 13 | ```jsx harmony 14 | import { Typography, Tag, Button } from 'antd'; 15 | 16 | import useDarkMode from 'beautiful-react-hooks/useDarkMode'; 17 | 18 | const UseDarkModeExample = () => { 19 | const { toggle, enable, disable, isDarkMode } = useDarkMode(); 20 | 21 | const Actions = [ 22 | 23 | Enable dark mode 24 | , 25 | 26 | Disable dark mode 27 | , 28 | 29 | Toggle dark mode 30 | 31 | ] 32 | 33 | return ( 34 | 35 | Click on the buttons to update isDarkMode flag 36 | isDarkMode: {isDarkMode ? 'true' : 'false'} 37 | 38 | ); 39 | }; 40 | 41 | 42 | ``` 43 | 44 | ### Mastering the hooks 45 | 46 | #### 🛑 When not to use 47 | 48 | - in server-only components (during SSR) 49 | 50 | 51 | ### Types 52 | 53 | ```typescript static 54 | export declare const LOCAL_STORAGE_KEY = "beautiful-react-hooks-is-dark-mode"; 55 | declare const useDarkMode: (defaultValue?: boolean, localStorageKey?: string) => Readonly; 56 | export interface UseDarkModeReturn { 57 | isDarkMode: boolean; 58 | toggle: () => void; 59 | enable: () => void; 60 | disable: () => void; 61 | } 62 | export default useDarkMode; 63 | 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/useDefaultedState.md: -------------------------------------------------------------------------------- 1 | # useDefaultedState 2 | 3 | A hook that functions similar to `useState`, with the added capability to receive a defaultValue and potentially an initialState.\ 4 | This hook guarantees that the state returned is always set to defaultValue in the event of it being null or undefined. 5 | 6 | ### Why? 💡 7 | 8 | - Avoids side-effects by ensuring a default state value 9 | 10 | ### Basic Usage: 11 | 12 | ```jsx harmony 13 | import { Typography, Button } from 'antd'; 14 | import useDefaultedState from 'beautiful-react-hooks/useDefaultedState'; 15 | 16 | /** 17 | * useDefaultedState example component 18 | */ 19 | const DefaultedStateExample = () => { 20 | const placeholder = { name: 'John Doe' }; 21 | const data = { name: 'Antonio Rù' }; 22 | const [user, setUser] = useDefaultedState(placeholder, data); 23 | 24 | const Actions = [ 25 | setUser()}>Change to 'undefined', 26 | ] 27 | 28 | return ( 29 | 30 | The user name is: {user.name} 31 | 32 | ); 33 | }; 34 | 35 | 36 | ``` 37 | 38 | ### Mastering the hook 39 | 40 | #### ✅ When to use 41 | 42 | - If you require a secure state that must never be null or undefined 43 | 44 | 45 | ### Types 46 | 47 | ```typescript static 48 | /** 49 | * Returns a safe state by making sure the given value is not null or undefined 50 | */ 51 | declare const useDefaultedState: (defaultValue: TValue, initialState?: TValue | undefined) => [TValue, (nextState: TValue) => void]; 52 | export default useDefaultedState; 53 | 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/useHorizontalSwipe.md: -------------------------------------------------------------------------------- 1 | # useHorizontalSwipe 2 | 3 | Returns the state of the horizontal swipe gesture both on mobile or desktop. 4 | It is intended as a shortcut to [useSwipe](./useSwipe.md). 5 | -------------------------------------------------------------------------------- /docs/useIsFirstRender.md: -------------------------------------------------------------------------------- 1 | # useIsFirstRender 2 | 3 | A hook that returns a boolean value indicating whether it's the first render or not. 4 | 5 | This hook can be used to conditionally execute logic or render components based on whether it's the first time the component is being 6 | rendered or if it's being re-rendered due to a state or prop change. 7 | 8 | ### 💡 Why? 9 | 10 | - A useful tool for managing component rendering behavior and enables you to write more efficient and flexible code 11 | 12 | ### Basic Usage: 13 | 14 | ```jsx harmony 15 | import { useState, useCallback } from 'react'; 16 | import { Button, Typography } from 'antd'; 17 | import useIsFirstRender from 'beautiful-react-hooks/useIsFirstRender'; 18 | 19 | const UseIsFirstRenderExample = () => { 20 | const [data, setData] = useState(0) 21 | const isFirstRender = useIsFirstRender(); 22 | 23 | const setNewDate = useCallback(() => setData(Date.now()), []); 24 | 25 | return ( 26 | 27 | Click on the button to update isFirstRender flag 28 | isFirstRender: {isFirstRender ? 'yes' : 'no'} 29 | 30 | Update data 31 | 32 | 33 | ); 34 | }; 35 | 36 | 37 | ``` 38 | 39 | 40 | ### Types 41 | 42 | ```typescript static 43 | declare const useIsFirstRender: () => boolean; 44 | export default useIsFirstRender; 45 | 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/useLocalStorage.md: -------------------------------------------------------------------------------- 1 | # useLocalStorage 2 | 3 | A hook that enables effortless storage and retrieval of values in the 4 | browser's [Local Storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). 5 | 6 | ### 💡 Why? 7 | 8 | - A fast and efficient method to implement the `localStorage` functionality in your React components 9 | 10 | ### Basic Usage: 11 | 12 | ```jsx harmony 13 | import React, { useCallback } from 'react'; 14 | import { Button, Tag, Typography } from 'antd'; 15 | import useLocalStorage from 'beautiful-react-hooks/useLocalStorage'; 16 | 17 | const NotificationBadgeExample = ({ notifications }) => { 18 | const [notificationCount, setNotificationCount] = useLocalStorage('demo-notification-count', notifications); 19 | 20 | const clearNotifications = useCallback(() => { 21 | setNotificationCount(0); 22 | }, [notificationCount]); 23 | 24 | const Actions = [ 25 | 26 | You've got {notificationCount} new messages 27 | 28 | ] 29 | 30 | return ( 31 | 32 | 33 | Click on the following button to clear data from the demo-notification-count local storage key. 34 | 35 | 36 | ) 37 | }; 38 | 39 | 40 | ``` 41 | 42 | ### Interface 43 | 44 | ```typescript 45 | type SetValue = (value: TValue | ((previousValue: TValue) => TValue)) => void 46 | 47 | declare const useLocalStorage: (storageKey: string, defaultValue?: any) => [TValue, SetValue] 48 | ``` 49 | 50 | ### Mastering the hooks 51 | 52 | #### ✅ When to use 53 | 54 | - When you need to get/set values from and to the `localStorage` 55 | 56 | #### 🛑 When not to use 57 | 58 | - Do not use this hook as a state manager, the `localStorage` is meant to be used for small pieces of data 59 | 60 | 61 | ### Types 62 | 63 | ```typescript static 64 | /** 65 | * Save a value on local storage 66 | */ 67 | declare const useLocalStorage: (storageKey: string, defaultValue?: any) => [TValue | null, (value: TValue | ((previousValue: TValue) => TValue)) => void]; 68 | export default useLocalStorage; 69 | 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/useMediaQuery.md: -------------------------------------------------------------------------------- 1 | # useMediaQuery 2 | 3 | A hook that takes in a media query string and utilizes the [matchMedia](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) 4 | API to check whether it corresponds to the present document. 5 | 6 | Additionally, it tracks changes in the document to detect when it no longer corresponds to the provided media query. 7 | 8 | The hook returns the validity status of the media query provided. 9 | 10 | ```jsx harmony 11 | import { Tag, Typography, Space, Alert } from 'antd'; 12 | import useMediaQuery from 'beautiful-react-hooks/useMediaQuery'; 13 | 14 | const MediaQueryReporter = () => { 15 | const isSmall = useMediaQuery('(max-width: 48rem)'); 16 | const isLarge = useMediaQuery('(min-width: 48rem)'); 17 | 18 | return ( 19 | 20 | 21 | 22 | Small view? {isSmall ? 'yes' : 'no'} 23 | Large view? {isLarge ? 'yes' : 'no'} 24 | 25 | 26 | ); 27 | }; 28 | 29 | 30 | ``` 31 | 32 | ### Mastering the hook 33 | 34 | #### ✅ When to use 35 | 36 | - If a component needs to display a different layout or behavior on various media types 37 | - Conditionally render sub-components based on a specified media query 38 | 39 | #### 🛑 When not to use 40 | 41 | - Avoid using this hook to identify the user's device, use agent detection instead 42 | 43 | 44 | ### Types 45 | 46 | ```typescript static 47 | /** 48 | * Accepts a media query string then uses the 49 | * [window.matchMedia](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) API to determine if it 50 | * matches with the current document. 51 | * It also monitor the document changes to detect when it matches or stops matching the media query. 52 | * Returns the validity state of the given media query. 53 | * 54 | */ 55 | declare const useMediaQuery: (mediaQuery: string) => boolean; 56 | export default useMediaQuery; 57 | 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/useMutableState.md: -------------------------------------------------------------------------------- 1 | # useMutableState 2 | 3 | This hook provides mutable states that trigger the component to re-render. It offers similar functionality to Svelte's reactivity, enabling 4 | developers to write more efficient and concise code. 5 | 6 | ### Why? 💡 7 | 8 | - Improves code streamlining by providing a reactive state that can be used to trigger a rerender 9 | 10 | ### Basic Usage: 11 | 12 | ```jsx harmony 13 | import { Typography, Space, Button, Tag } from 'antd'; 14 | import useMutableState from 'beautiful-react-hooks/useMutableState'; 15 | 16 | const TestComponent = () => { 17 | const counter = useMutableState({ value: 0 }); 18 | 19 | return ( 20 | 21 | 22 | Counter: {counter.value} 23 | 24 | 25 | counter.value += 1}>increase 26 | counter.value -= 1}>decrease 27 | 28 | 29 | ); 30 | }; 31 | 32 | 33 | ``` 34 | 35 | 36 | -------------------------------------------------------------------------------- /docs/useMutationObserver.md: -------------------------------------------------------------------------------- 1 | # useMutationObserver 2 | 3 | A hook that employs the [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) API to monitor changes made to 4 | the Document Object Model (DOM) tree. 5 | 6 | This hook enables asynchronous observation of changes to a specified HTML Element. It automatically handles the clean-up of the observation 7 | process when the associated component is unmounted. 8 | 9 | ### Why? 💡 10 | 11 | - allows for real-time monitoring of changes to the DOM, without requiring constant polling or manual inspection of the element. 12 | - provides more granular control over the types of changes being observed, allowing developers to selectively listen for specific events 13 | such as attribute modifications, node insertions or removals, and so on. 14 | 15 | ### Basic Usage: 16 | 17 | ```jsx harmony 18 | import { useRef, useState } from 'react'; 19 | import { Tag, Typography } from 'antd'; 20 | import useMutationObserver from 'beautiful-react-hooks/useMutationObserver'; 21 | 22 | const UseMutationObserverExample = () => { 23 | const ref = useRef(); 24 | const [content, setContent] = useState('Hello world'); 25 | const [mutationCount, setMutationCount] = useState(0); 26 | 27 | const incrementMutationCount = () => 28 | setMutationCount((prev) => prev + 1); 29 | 30 | useMutationObserver(ref, incrementMutationCount); 31 | 32 | return ( 33 | 34 | 35 | 47 | Resize me 48 | 49 | 50 | Mutations: {mutationCount} 51 | 52 | ); 53 | }; 54 | 55 | 56 | ``` 57 | 58 | 59 | ### Types 60 | 61 | ```typescript static 62 | import { type RefObject } from 'react'; 63 | declare const useMutationObserver: (ref: RefObject, callback: MutationCallback, options?: MutationObserverInit) => void; 64 | export default useMutationObserver; 65 | 66 | ``` 67 | -------------------------------------------------------------------------------- /docs/useObjectState.md: -------------------------------------------------------------------------------- 1 | # useObjectState 2 | 3 | A hook has been developed to emulate the behavior of the now deprecated class Component.setState method. This hook aims to facilitate the 4 | migration process of legacy class components to the new function components paradigm. 5 | 6 | ### Why? 💡 7 | 8 | - Automates the process of destructing the previous state and replacing it with a new one, alleviating the burden of manually handling these 9 | operations in function components 10 | - Allow developers to seamlessly transition their codebase from class components to function components without needing to restructure the 11 | existing codebase 12 | 13 | ### Basic Usage: 14 | 15 | ```jsx harmony 16 | import { useState, useRef } from 'react'; 17 | import { Button, Typography } from 'antd'; 18 | import useObjectState from 'beautiful-react-hooks/useObjectState'; 19 | 20 | const UseObjectStateComponent = () => { 21 | const [state, setState] = useObjectState({ count: 0, title: 'Test title' }) 22 | 23 | const reset = () => setState({ count: 0 }) 24 | 25 | const increment = () => setState({ count: state.count + 1 }) 26 | 27 | const decrement = () => setState({ count: state.count - 1 }) 28 | 29 | const Actions = [ 30 | 31 | Increment counter 32 | , 33 | 34 | Decrement counter 35 | , 36 | 37 | Reset counter 38 | 39 | ] 40 | 41 | return ( 42 | 43 | State: 44 | {JSON.stringify(state, null, '\t')} 45 | 46 | ); 47 | }; 48 | 49 | 50 | ``` 51 | 52 | ### Mastering the hook 53 | 54 | #### ✅ When to use 55 | 56 | - When required to migrate legacy class components to the new function components paradigm 57 | 58 | #### 🛑 What not to do 59 | 60 | - Don't use this hook in place of `useReducer`. 61 | 62 | 63 | ### Types 64 | 65 | ```typescript static 66 | declare const useObjectState: (initialState: TState) => [TState, (state: Partial) => void]; 67 | export default useObjectState; 68 | 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/useOnlineState.md: -------------------------------------------------------------------------------- 1 | # useOnlineState 2 | 3 | A hook is available in this library that utilizes 4 | the [Navigator online API](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine) to determine the connectivity status of 5 | the user's browser. 6 | 7 | This hook returns a boolean value which indicates whether the browser is currently online or offline. 8 | 9 | The primary use case for this hook is to facilitate re-rendering of a component when the browser's connectivity status changes. By using 10 | this hook, your component can respond dynamically to changes in connectivity and update its behavior accordingly 11 | 12 | ### Why? 💡 13 | 14 | - Your component requires network connectivity to function correctly and should behave differently when offline 15 | - You need to trigger some functionality when the user's connectivity status changes, such as syncing data with a server when the user comes 16 | back online 17 | 18 | ### Basic Usage: 19 | 20 | ```jsx harmony 21 | import { Tag, Typography } from 'antd'; 22 | import useOnlineState from 'beautiful-react-hooks/useOnlineState'; 23 | 24 | const ConnectionTest = () => { 25 | const isOnline = useOnlineState(); 26 | 27 | return ( 28 | 29 | 30 | Connection status: {isOnline ? 'online' : 'offline'} 31 | 32 | 33 | ); 34 | }; 35 | 36 | 37 | ``` 38 | 39 | 40 | ### Types 41 | 42 | ```typescript static 43 | /** 44 | * Uses the [Navigator online API](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine) to define 45 | * whether the browser is connected or not. 46 | */ 47 | declare const useOnlineState: () => boolean; 48 | export default useOnlineState; 49 | 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/usePreviousValue.md: -------------------------------------------------------------------------------- 1 | # usePreviousValue 2 | 3 | A hook that receives a variable, which can be either a prop or a state, and outputs its previous value from the last render cycle 4 | 5 | ### Why? 💡 6 | 7 | - Enables monitoring of changes to component state/props 8 | - Facilitates informed decisions on when to trigger component updates 9 | 10 | ### Basic Usage: 11 | 12 | ```jsx harmony 13 | import { useState } from 'react'; 14 | import { Typography, Tag } from 'antd'; 15 | import useInterval from 'beautiful-react-hooks/useInterval'; 16 | import usePreviousValue from 'beautiful-react-hooks/usePreviousValue'; 17 | 18 | const TestComponent = () => { 19 | const [seconds, setSeconds] = useState(0); 20 | const prevSeconds = usePreviousValue(seconds); 21 | 22 | useInterval(() => setSeconds(1 + seconds), 1000); 23 | 24 | return ( 25 | 26 | 27 | {seconds}s 28 | 29 | 30 | The previous value of the state 'seconds' was: {prevSeconds} 31 | 32 | 33 | ); 34 | }; 35 | 36 | 37 | ``` 38 | 39 | 40 | ### Types 41 | 42 | ```typescript static 43 | /** 44 | * On each render returns the previous value of the given variable/constant. 45 | */ 46 | declare const usePreviousValue: (value?: TValue | undefined) => TValue | undefined; 47 | export default usePreviousValue; 48 | 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/useQueryParam.md: -------------------------------------------------------------------------------- 1 | # useQueryParam 2 | 3 | A hook built on top of React Router v5 that facilitate access and manipulation of query parameters. 4 | 5 | ### Why? 💡 6 | 7 | - Facilitates editing the query string in the URL for the current location 8 | - Functions similarly to the useState hook 9 | - Does not rely on version 6 of the useSearchParams function from react-router-dom, ensuring compatibility with older versions 10 | 11 | ### Basic Usage: 12 | 13 | ```jsx harmony 14 | import { useState, useRef } from 'react'; 15 | import { HashRouter as Router } from 'react-router-dom' 16 | import { Typography, Tag, Input } from 'antd'; 17 | import useQueryParam from 'beautiful-react-hooks/useQueryParam'; 18 | 19 | const ExampleComponent = () => { 20 | // second parameter is optional 21 | const [param, setValue] = useQueryParam('foo', { 22 | initialValue: 'bar', 23 | replaceState: false, 24 | }) 25 | 26 | return ( 27 | 28 | 29 | Current value of 'foo' param is {param}< 30 | /Typography.Paragraph> 31 | setValue(e.targt.value)} /> 32 | 33 | ); 34 | }; 35 | 36 | 37 | 38 | 39 | ``` 40 | 41 | 42 | ### Types 43 | 44 | ```typescript static 45 | export interface UseQueryParamOptions { 46 | initialValue?: TValue; 47 | replaceState?: boolean; 48 | } 49 | /** 50 | * Ease the process of modify the query string in the URL for the current location. 51 | */ 52 | declare const useQueryParam: (key: string, options?: UseQueryParamOptions) => [TValue, (nextValue?: TValue | undefined) => void]; 53 | export default useQueryParam; 54 | 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/useQueryParams.md: -------------------------------------------------------------------------------- 1 | # useQueryParams 2 | 3 | Very similar to `useQueryParam` (mind the final 's'), it eases the process of manipulate a query string with multiple values. 4 | 5 | ### Why? 💡 6 | 7 | - Ease the process of manipulate a query string (with multiple values) in the URL for the current location. 8 | - Works similar to the useState hook 9 | - it's NOT built on top of version 6 of react-router-dom's useSearchParams, it is therefore compatible with older version 10 | 11 | ### Basic Usage: 12 | 13 | ```jsx harmony 14 | import { useState, useRef } from 'react'; 15 | import { HashRouter as Router } from 'react-router-dom' 16 | import { Button, Typography, Input } from 'antd' 17 | import useQueryParams from 'beautiful-react-hooks/useQueryParams'; 18 | 19 | const ExampleComponent = () => { 20 | // second parameter is optional 21 | const [foos, setFoos] = useQueryParams('foo[]', { 22 | initialValue: [1, 2, 3], 23 | replaceState: false, 24 | }) 25 | 26 | const onClick = () => setFoos([4, 5, 6]) 27 | 28 | const Actions = [ 29 | 30 | Change to param to [4,5,6] 31 | 32 | ] 33 | 34 | return ( 35 | 36 | 37 | Current value of 'foo[]' param is '{foos.join(',')}' 38 | 39 | 40 | ); 41 | }; 42 | 43 | 44 | 45 | 46 | ``` 47 | 48 | 49 | ### Types 50 | 51 | ```typescript static 52 | export interface UseQueryParamsOptions { 53 | initialValue?: TValue; 54 | replaceState?: boolean; 55 | } 56 | /** 57 | * Very similar to `useQueryParams`, it eases the process of manipulate a query string that handles multiple values 58 | */ 59 | declare const useQueryParams: (key: string, options?: UseQueryParamsOptions) => [TValue, (nextValue?: TValue | undefined) => void]; 60 | export default useQueryParams; 61 | 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/useRenderInfo.md: -------------------------------------------------------------------------------- 1 | # useRenderInfo 2 | 3 | A hook that prints the number of renders for a given component, along with a timestamp of the most recent render and the time elapsed since 4 | the last render. 5 | 6 | ### Why? 💡 7 | 8 | - Easily display information on components render 9 | 10 | ### Basic Usage: 11 | 12 | ```jsx harmony 13 | import { Typography } from 'antd'; 14 | import useInterval from 'beautiful-react-hooks/useInterval'; 15 | import useRenderInfo from 'beautiful-react-hooks/useRenderInfo'; 16 | 17 | const RenderInfo = () => { 18 | const [seconds, setSeconds] = React.useState(0); 19 | 20 | // repeat the function each 1000ms 21 | useInterval(() => { 22 | setSeconds(1 + seconds); 23 | }, 1000); 24 | 25 | useRenderInfo('Module'); 26 | 27 | return ( 28 | 29 | Check the console! 30 | 31 | ); 32 | }; 33 | 34 | 35 | ``` 36 | 37 | ### Custom logs: 38 | 39 | ```jsx harmony 40 | import { Typography } from 'antd'; 41 | import useInterval from 'beautiful-react-hooks/useInterval'; 42 | import useRenderInfo from 'beautiful-react-hooks/useRenderInfo'; 43 | 44 | const RenderInfo = () => { 45 | const [seconds, setSeconds] = React.useState(0); 46 | const info = useRenderInfo(); 47 | 48 | // repeat the function each 1000ms 49 | useInterval(() => { 50 | setSeconds(1 + seconds); 51 | }, 1000); 52 | 53 | return ( 54 | 55 | {info.sinceLast} seconds passed from the last render! 56 | 57 | ); 58 | }; 59 | 60 | 61 | ``` 62 | 63 | ### Mastering the hook 64 | 65 | #### ✅ When to use 66 | 67 | - When debugging a component 68 | 69 | #### 🛑 What not to do 70 | 71 | - In production build, you don't want useless logs in console :) 72 | 73 | 74 | ### Types 75 | 76 | ```typescript static 77 | export interface RenderInfo { 78 | readonly module: string; 79 | renders: number; 80 | timestamp: null | number; 81 | sinceLast: null | number | '[now]'; 82 | } 83 | /** 84 | * useRenderInfo 85 | * @param moduleName 86 | * @param log 87 | * @returns {{renders: number, module: *, timestamp: null}} 88 | */ 89 | declare const useRenderInfo: (moduleName?: string, log?: boolean) => RenderInfo; 90 | export default useRenderInfo; 91 | 92 | ``` 93 | -------------------------------------------------------------------------------- /docs/useSearchQuery.md: -------------------------------------------------------------------------------- 1 | # useSearchQuery 2 | 3 | A hook built on top of React Router v5 that facilitate access and manipulation of the 'search' query parameter. 4 | 5 | ### Why? 💡 6 | 7 | - Facilitates editing the 'search' query string in the URL for the current location 8 | - Functions similarly to the useState hook 9 | 10 | ### Basic Usage: 11 | 12 | ```jsx harmony 13 | import { useState, useRef } from 'react'; 14 | import { HashRouter as Router } from 'react-router-dom' 15 | import { Input, Typography, Tag } from 'antd' 16 | import useSearchQuery from 'beautiful-react-hooks/useSearchQuery'; 17 | 18 | const ExampleComponent = () => { 19 | const [searchValue, setSearch] = useSearchQuery('initial-value') 20 | 21 | return ( 22 | 23 | 24 | Current value of search param is {searchValue} 25 | 26 | setSearch(nextValue)} /> 27 | 28 | ); 29 | }; 30 | 31 | 32 | 33 | 34 | ``` 35 | 36 | 37 | ### Types 38 | 39 | ```typescript static 40 | /** 41 | * Ease the process of modify the 'search' query string in the URL for the current location. 42 | * It's just a shortcut/wrapper around useQueryParam 43 | */ 44 | declare const useSearchQuery: (initialValue?: TSearchKey | undefined, replaceState?: boolean) => [TSearchKey, (nextValue?: TSearchKey | undefined) => void]; 45 | export default useSearchQuery; 46 | 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/useSessionStorage.md: -------------------------------------------------------------------------------- 1 | # useSessionStorage 2 | 3 | A hook that enables effortless storage and retrieval of values in the 4 | browser's [Session Storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage). 5 | 6 | ### 💡 Why? 7 | 8 | - A fast and efficient method to implement the `sessionStorage` functionality in your React components 9 | 10 | ### Basic Usage: 11 | 12 | ```jsx harmony 13 | import React, { useCallback } from 'react'; 14 | import { Pill, Paragraph, Icon } from 'antd'; 15 | import useSessionStorage from 'beautiful-react-hooks/useSessionStorage'; 16 | 17 | const NotificationBadgeExample = ({ notifications }) => { 18 | const [notificationCount, setNotificationCount] = useSessionStorage('demo-notification-count', notifications); 19 | 20 | const clearNotifications = useCallback(() => { 21 | setNotificationCount(0); 22 | }, [notificationCount]); 23 | 24 | const Actions = [ 25 | 26 | You've got {notificationCount} new messages 27 | 28 | ] 29 | 30 | return ( 31 | 32 | 33 | Click on the following button to clear data from the demo-notification-count session storage key. 34 | 35 | 36 | ) 37 | }; 38 | 39 | 40 | ``` 41 | 42 | ### Interface 43 | 44 | ```typescript 45 | type SetValue = (value: TValue | ((previousValue: TValue) => TValue)) => void 46 | 47 | declare const useSessionStorage: (storageKey: string, defaultValue?: any) => [TValue, SetValue] 48 | ``` 49 | 50 | ### Mastering the hooks 51 | 52 | #### ✅ When to use 53 | 54 | - When you need to get/set values from and to the `sessionStorage` 55 | 56 | #### 🛑 When not to use 57 | 58 | - Do not use this hook as a state manager, the `sessionStorage` is meant to be used for small pieces of data 59 | 60 | 61 | ### Types 62 | 63 | ```typescript static 64 | /** 65 | * Save a value on session storage 66 | */ 67 | declare const useSessionStorage: (storageKey: string, defaultValue?: any) => [TValue | null, (value: TValue | ((previousValue: TValue) => TValue)) => void]; 68 | export default useSessionStorage; 69 | 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/useSpeechRecognition.md: -------------------------------------------------------------------------------- 1 | # useSpeechSynthesis 2 | 3 | A hook that provides an interface for using the [Web_Speech_API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API) to 4 | recognize and transcribe speech in a user's browser. 5 | 6 | ### Why? 💡 7 | 8 | - Abstracts the implementation details of the Web Speech API into a single reusable function. 9 | 10 | ### Basic Usage: 11 | 12 | ```jsx harmony 13 | import { Button, Space, Tag, Typography, Input } from 'antd'; 14 | import useSpeechRecognition from 'beautiful-react-hooks/useSpeechRecognition'; 15 | 16 | const SpeechSynthesisDemo = () => { 17 | const [name, setName] = React.useState('Antonio'); 18 | const { startRecording, transcript, stopRecording, isRecording, isSupported } = useSpeechRecognition(); 19 | 20 | return ( 21 | 22 | 23 | 24 | Supported: {isSupported ? 'Yes' : 'No'} 25 | 26 | 27 | {isRecording ? 'Stop' : 'Start'} recording 28 | 29 | 30 | {transcript} 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | 38 | ``` 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/useSystemVoices.md: -------------------------------------------------------------------------------- 1 | # useSystemVoices 2 | 3 | A hook that returns all the available voices on the system. 4 | 5 | **Note:** it's important to note that the purpose of this hook is to maintain backward compatibility with a previous version of the library 6 | that utilized a non-stable version of the Web Speech API. In that version, voices were returned asynchronously. 7 | 8 | If you are currently using a version of the library that does not require this hook, you can simply run: 9 | 10 | ```typescript static 11 | const voices = window.speechSynthesis.getVoices() 12 | ``` 13 | 14 | ### Why? 💡 15 | 16 | - At the moment, the `window.speechSynthesis.getVoices` function returns all the available system voices, but since 17 | it does it 18 | asynchronously [the returning value is an empty array until a second call is performed](https://w3c.github.io/speech-api/speechapi-errata.html) 19 | this hook manage the side-effect of correctly retrieve all the available system voices. 20 | 21 | ### Basic Usage: 22 | 23 | ```jsx harmony 24 | import { List, Typography } from 'antd'; 25 | import useSystemVoices from 'beautiful-react-hooks/useSystemVoices'; 26 | 27 | const SpeechSynthesisDemo = () => { 28 | const voices = useSystemVoices(); 29 | 30 | return ( 31 | 32 | System voices 33 | 34 | {voices.map(({ name, lang }) => {name} - {lang})} 35 | 36 | 37 | ); 38 | }; 39 | 40 | 41 | ``` 42 | 43 | 44 | ### Types 45 | 46 | ```typescript static 47 | /** 48 | * Returns all the available voices on the system. 49 | * This hook is here to backward compatibility with the previous version of the library that was using 50 | * a different non-stable version of the Web Speech API. 51 | */ 52 | declare const useSystemVoices: () => SpeechSynthesisVoice[]; 53 | export default useSystemVoices; 54 | 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/useToggle.md: -------------------------------------------------------------------------------- 1 | # useToggle 2 | 3 | A hook that encapsulates the business logic of dealing with boolean values. 4 | 5 | Provides a higher-level interface for dealing with boolean logic in React function component. 6 | 7 | ### Why? 💡 8 | 9 | - Having multiple boolean states in an application often leads to code redundancy. This hook consolidates the implementation details of a 10 | singular boolean state, promoting code reusability and reducing code bloat. 11 | 12 | ### Basic Usage: 13 | 14 | ```jsx harmony 15 | import { Typography, Tag, Button } from 'antd' 16 | import useToggle from 'beautiful-react-hooks/useToggle' 17 | 18 | const ComponentWillUnmount = () => { 19 | const [value, toggleValue] = useToggle() 20 | const tagColor = value ? 'green' : 'red' 21 | 22 | return ( 23 | 24 | 25 | Toggle is {value ? 'on' : 'off'} 26 | 27 | toggle value 28 | 29 | ); 30 | }; 31 | 32 | 33 | ``` 34 | 35 | ### Initial state 36 | 37 | ```jsx harmony 38 | import { Button, Typography, Tag } from 'antd' 39 | import useToggle from 'beautiful-react-hooks/useToggle' 40 | 41 | const ComponentWillUnmount = () => { 42 | const [value, toggleValue] = useToggle(true) 43 | const tagColor = value ? 'green' : 'red' 44 | 45 | return ( 46 | 47 | 48 | Toggle is {value ? 'on' : 'off'} 49 | 50 | toggle value 51 | 52 | ); 53 | }; 54 | 55 | 56 | ``` 57 | 58 | 59 | ### Types 60 | 61 | ```typescript static 62 | /** 63 | * A quick and simple utility for toggle states 64 | */ 65 | declare const useToggle: (initialState?: boolean) => [boolean, () => void]; 66 | export default useToggle; 67 | 68 | ``` 69 | -------------------------------------------------------------------------------- /docs/useURLSearchParams.md: -------------------------------------------------------------------------------- 1 | # useURLSearchParams 2 | 3 | A hook that encapsulates the functionality of retrieving an always updated URLSearchParams object. 4 | 5 | ### Why? 💡 6 | 7 | - simplify the process of obtaining an always up-to-date instance of the URLSearchParams object 8 | - This hook is not based on the useSearchParams hook from version **6** of the `react-router-dom` library. Therefore, it is compatible with 9 | earlier versions of `react-router-dom` 10 | 11 | ### Basic Usage: 12 | 13 | ```jsx harmony 14 | import { useState, useRef } from 'react'; 15 | import { HashRouter as Router, useHistory } from 'react-router-dom' 16 | import { Button, Input } from 'antd' 17 | import useURLSearchParams from 'beautiful-react-hooks/useURLSearchParams' 18 | import useDidMount from 'beautiful-react-hooks/useDidMount' 19 | 20 | const ExampleComponent = () => { 21 | const history = useHistory() 22 | const params = useURLSearchParams() 23 | const onMount = useDidMount() 24 | 25 | onMount(() => { 26 | params.set('foo', 'value') 27 | 28 | history.replace({ 29 | search: params.toString(), 30 | }) 31 | }) 32 | 33 | return ( 34 | 35 | Current value of 'foo' param is '{params.get('foo')}' 36 | Change the value of the foo param to see how this hook works 37 | 38 | ); 39 | }; 40 | 41 | 42 | 43 | 44 | ``` 45 | 46 | 47 | ### Types 48 | 49 | ```typescript static 50 | /** 51 | * Wraps the business logic of retrieve always updated URLSearchParams 52 | */ 53 | declare const useURLSearchParams: () => URLSearchParams; 54 | export default useURLSearchParams; 55 | 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/useUpdateEffect.md: -------------------------------------------------------------------------------- 1 | # useUpdateEffect 2 | 3 | A hook that modifies the behavior of the `useEffect` hook by skipping the initial render. This hook is particularly useful in cases where 4 | the effect should only run after the first update of the component, but not during the initial mount. 5 | 6 | ### Basic Usage: 7 | 8 | ```jsx harmony 9 | import { useState, useEffect, useCallback } from 'react'; 10 | import { Alert, Space, Button } from 'antd'; 11 | import useUpdateEffect from 'beautiful-react-hooks/useUpdateEffect'; 12 | 13 | const UseUpdateEffectExample = () => { 14 | const [data, setData] = useState(0) 15 | 16 | useEffect(() => { 17 | console.log('Normal useEffect', { data }) 18 | }, [data]) 19 | 20 | useUpdateEffect(() => { 21 | console.log('Update useEffect only', { data }) 22 | }, [data]) 23 | 24 | const setNewDate = useCallback(() => setData(Date.now()), []); 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | Update data 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | 39 | ``` 40 | 41 | 42 | ### Types 43 | 44 | ```typescript static 45 | import { type DependencyList, type EffectCallback } from 'react'; 46 | /** 47 | * A hook that runs an effect after the first render. 48 | * @param callback 49 | * @param deps 50 | */ 51 | declare const useUpdateEffect: (callback: EffectCallback, deps?: DependencyList) => void; 52 | export default useUpdateEffect; 53 | 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/useValidatedState.md: -------------------------------------------------------------------------------- 1 | # useValidatedState 2 | 3 | This hook is similar to useState but accepts a validator function as first parameter and the initial state value as second, then returns the 4 | state array where the third parameter is result of the validation. 5 | 6 | ### Why? 💡 7 | 8 | - You want to have information on a state validation. 9 | 10 | ### Basic Usage: 11 | 12 | ```jsx harmony 13 | import { Input, Space, Typography } from 'antd'; 14 | import useValidatedState from 'beautiful-react-hooks/useValidatedState'; 15 | 16 | const passwordValidator = (password) => password.length > 3; 17 | 18 | const ValidatedField = () => { 19 | const [password, setPassword, validation] = useValidatedState(passwordValidator, 'sk8'); 20 | 21 | return ( 22 | 23 | 24 | setPassword(e.target.value)} 27 | status={!validation.valid && 'error'} 28 | placeholder="Insert password" 29 | /> 30 | 31 | {validation.valid ? 'Password is valid' : 'Password is too short'} 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | 39 | ``` 40 | 41 | ### Mastering the hook 42 | 43 | #### ✅ good to know: 44 | 45 | - useValidatedState does not re-render your component twice to save the validation state. 46 | 47 | 48 | ### Types 49 | 50 | ```typescript static 51 | /** 52 | * Returns a state that changes only if the next value pass its validator 53 | */ 54 | declare const useValidatedState: >(validator: TValidator, initialValue?: TValue | undefined) => [TValue, (nextValue: TValue) => void, ValidationResult]; 55 | export type Validator = (value: TValue) => boolean; 56 | export interface ValidationResult { 57 | changed: boolean; 58 | valid?: boolean; 59 | } 60 | export default useValidatedState; 61 | 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/useValueHistory.md: -------------------------------------------------------------------------------- 1 | # useValueHistory 2 | 3 | A hook that takes a variable, which can be a prop or a state, and returns an array of its previous values. This hook is useful for tracking 4 | changes in a variable across multiple renders and allows developers to compare the current value with its previous values. 5 | 6 | Overall, the "usePrevious" hook is a helpful tool for debugging and improving the performance of React components that rely on the history 7 | of a specific variable 8 | 9 | ### Basic Usage: 10 | 11 | ```jsx harmony 12 | import { useState } from 'react'; 13 | import { Tag, Typography } from 'antd'; 14 | import useValueHistory from 'beautiful-react-hooks/useValueHistory'; 15 | import useInterval from 'beautiful-react-hooks/useInterval'; 16 | 17 | const TestComponent = () => { 18 | const [count, setCount] = useState(0); 19 | const countHistory = useValueHistory(count); 20 | 21 | useInterval(() => setCount(1 + count), 500); 22 | 23 | return ( 24 | 25 | Count: {count} 26 | The history of the `count` state is: 27 | 28 | {countHistory.join(', ')} 29 | 30 | 31 | ); 32 | }; 33 | 34 | 35 | ``` 36 | 37 | 38 | ### Types 39 | 40 | ```typescript static 41 | /** 42 | * Accepts a variable (possibly a prop or a state) and returns its history (changes through updates). 43 | */ 44 | declare const useValueHistory: (value: TValue, distinct?: boolean) => TValue[]; 45 | export default useValueHistory; 46 | 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/useVerticalSwipe.md: -------------------------------------------------------------------------------- 1 | # useVerticalSwipe 2 | 3 | Returns the state of the vertical swipe gesture both on mobile or desktop. 4 | It is intended as a shortcut to [useSwipe](./useSwipe.md). 5 | -------------------------------------------------------------------------------- /docs/useViewportState.md: -------------------------------------------------------------------------------- 1 | # useViewportState 2 | 3 | A hook that returns relevant information on the current viewport state. 4 | 5 | It's built on top of [useWindowResize](./useWindowResize.md) and [useWindowScroll](./useWindowScroll.md). 6 | 7 | ### Why? 💡 8 | 9 | - takes care of adding the listener for the window resize event. 10 | - takes care of removing the listener when the component will unmount 11 | 12 | ### Basic Usage: 13 | 14 | ```jsx harmony 15 | import { useState } from 'react'; 16 | import { Typography, Tag } from 'antd'; 17 | import useViewportState from 'beautiful-react-hooks/useViewportState'; 18 | 19 | const WindowSizeReporter = () => { 20 | const { width, height, scrollX, scrollY } = useViewportState(); 21 | 22 | return ( 23 | 24 | Window current properties 25 | width: {width} 26 | height: {height} 27 | horizontal scroll: {scrollX} 28 | vertical scroll: {scrollY} 29 | 30 | ); 31 | }; 32 | 33 | 34 | ``` 35 | 36 | ### Mastering the hook 37 | 38 | #### ✅ When to use 39 | 40 | - When in need of reading common information about the current viewport 41 | 42 | 43 | ### Types 44 | 45 | ```typescript static 46 | export interface ViewportState { 47 | width: number; 48 | height: number; 49 | scrollX: number; 50 | scrollY: number; 51 | } 52 | /** 53 | * Returns updated information on the current viewport state 54 | */ 55 | declare const useViewportState: (debounceBy?: number) => ViewportState; 56 | export default useViewportState; 57 | 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/utils/_CustomLogo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Styled from 'rsg-components/Styled' 4 | import logo from './_doc-logo.png' 5 | 6 | const styles = ({ fontFamily, color }) => ({ 7 | logo: { 8 | display: 'block' 9 | }, 10 | image: { 11 | width: '100%' 12 | } 13 | }) 14 | 15 | const LogoRenderer = ({ classes }) => ( 16 | 17 | 18 | 19 | ) 20 | 21 | LogoRenderer.propTypes = { 22 | classes: PropTypes.object.isRequired, 23 | children: PropTypes.node 24 | } 25 | 26 | export default Styled(styles)(LogoRenderer) 27 | -------------------------------------------------------------------------------- /docs/utils/_EmptyComponent.js: -------------------------------------------------------------------------------- 1 | export default () => null; 2 | -------------------------------------------------------------------------------- /docs/utils/_custom.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Ubuntu:300,300i,400,400i,500,500i,700,700i&display=swap'); 2 | @import '~antd/dist/reset.css'; 3 | 4 | body { 5 | font-family: 'Ubuntu', sans-serif; 6 | } 7 | -------------------------------------------------------------------------------- /docs/utils/_doc-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioru/beautiful-react-hooks/21399d635bd75679aac7d53821dd3e54c93d635c/docs/utils/_doc-logo.png -------------------------------------------------------------------------------- /docs/utils/_setup.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const { Card } = require('antd') 3 | 4 | const DisplayDemo = window.DisplayDemo = (props) => ( 5 | React.createElement(Card, { bordered: true, hoverable: true, ...props }, props.children) 6 | ) 7 | -------------------------------------------------------------------------------- /docs/utils/_styleguidist.theme.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // https://github.com/styleguidist/react-styleguidist/blob/master/src/client/styles/theme.ts 3 | theme: { 4 | color: { 5 | base: '#2D3142', 6 | text: '#2D3142', 7 | link: '#1D6C8B', 8 | linkHover: '#317995' 9 | }, 10 | baseColor: '#2D3142', 11 | fontFamily: { 12 | base: '"Ubuntu", "sans-serif", light', 13 | }, 14 | }, 15 | styles: { 16 | SectionHeading: { 17 | wrapper: { 18 | display: 'none', 19 | }, 20 | }, 21 | Code: { 22 | code: { 23 | fontFamily: '\'Ubuntu Mono\', sans-serif', 24 | backgroundColor: '#CF7A95', 25 | color: '#fff', 26 | fontWeight: '400', 27 | padding: '0 5px', 28 | }, 29 | }, 30 | Para: { 31 | para: { 32 | fontFamily: '\'Ubuntu\', sans-serif', 33 | }, 34 | }, 35 | StyleGuide: { 36 | sidebar: { 37 | border: 0, 38 | width: '16rem', 39 | background: 'white', 40 | boxShadow: '0 0 20px 0 rgba(20, 20, 20, 0.1)', 41 | }, 42 | content: { 43 | maxWidth: '960px', 44 | }, 45 | root: { 46 | background: '#FBFAF9', 47 | }, 48 | hasSidebar: { 49 | paddingLeft: '16rem', 50 | } 51 | }, 52 | Playground: { 53 | preview: { 54 | padding: 0, 55 | border: 'none', 56 | background: 'transparent', 57 | //borderRadius: '6px', 58 | // boxShadow: '0 0px 10px 0 rgba(93, 100, 148, 0.05)', 59 | }, 60 | }, 61 | }, 62 | } 63 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioru/beautiful-react-hooks/21399d635bd75679aac7d53821dd3e54c93d635c/logo.png -------------------------------------------------------------------------------- /scripts/commit-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd .. 4 | git add package.json 5 | git commit -m "chore(release): Updates package.json version to $1 [skip ci]" 6 | git push origin master 7 | -------------------------------------------------------------------------------- /scripts/generate-doc-append-types.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs/promises') 2 | const path = require('path') 3 | const { globSync } = require('glob') 4 | 5 | const docsPath = path.join(__dirname, '..', 'docs') 6 | const distPath = path.join(__dirname, '..', 'dist') 7 | const docFiles = globSync(`${docsPath}/*.md`) 8 | .map((file) => file.replace(`${docsPath}/`, '').replace('.md', '')) 9 | .filter((file) => file.startsWith('use')) 10 | 11 | docFiles.forEach(async (hook) => { 12 | const docPath = path.join(docsPath, `${hook}.md`) 13 | const typeFile = path.join(distPath, `${hook}.d.ts`) 14 | 15 | const declarations = await fs.readFile(typeFile, { encoding: 'utf8' }) 16 | const document = await fs.readFile(docPath, { encoding: 'utf8' }) 17 | 18 | if (document.match(//g)) { 19 | const cleared = emptyOldTypes(document).trim() 20 | const nextDocument = cleared.replace('', template(declarations)).trim() 21 | 22 | fs.writeFile(docPath, nextDocument, { encoding: 'utf8' }).then(() => { 23 | console.log(`Updated "${hook}" types`) 24 | }) 25 | } 26 | }) 27 | 28 | const template = (content) => ` 29 | ### Types 30 | 31 | \`\`\`typescript static 32 | ${content} 33 | \`\`\` 34 | 35 | ` 36 | 37 | const emptyOldTypes = (content) => { 38 | const regex = /[\s\S]*/g 39 | 40 | return `${content.replace(regex, '\n').replace(//g, '').trim()} 41 | 42 | 43 | ` 44 | } 45 | -------------------------------------------------------------------------------- /scripts/generate-exports.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const { globSync } = require('glob') 6 | 7 | const srcPath = path.join(__dirname, '..', 'src') 8 | const pkgPath = path.join(__dirname, '..', 'package.json') 9 | 10 | const srcFiles = globSync(`${srcPath}/*.ts`) 11 | .map((file) => file.replace(`${srcPath}/`, '').replace('.ts', '')) 12 | .filter((file) => file !== 'index') 13 | 14 | const exportsObj = srcFiles.reduce((acc, file) => ({ 15 | ...acc, 16 | [`./${file}`]: { 17 | import: `./esm/${file}.js`, 18 | require: `./${file}.js`, 19 | types: `./${file}.d.ts` 20 | } 21 | }), {}) 22 | 23 | const packageJsonText = fs.readFileSync(pkgPath) 24 | const packageJson = JSON.parse(packageJsonText) 25 | 26 | const nextPackageJson = { ...packageJson, exports: exportsObj } 27 | 28 | console.log('\nUPDATING EXPORTS: ', Object.keys(exportsObj)) 29 | 30 | fs.writeFileSync(pkgPath, JSON.stringify(nextPackageJson, null, 2)) 31 | -------------------------------------------------------------------------------- /scripts/update-version.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | const nextVersion = process.argv[2] 7 | const pkgPath = path.join(__dirname, '..', 'package.json') 8 | 9 | const packageJsonText = fs.readFileSync(pkgPath) 10 | const packageJson = JSON.parse(packageJsonText) 11 | 12 | const nextPackageJson = { ...packageJson, version: nextVersion } 13 | 14 | console.log('\nUPDATING PACKAGE JSON VERSION TO: ', nextVersion) 15 | 16 | fs.writeFileSync(pkgPath, JSON.stringify(nextPackageJson, null, 2)) 17 | -------------------------------------------------------------------------------- /src/factory/createHandlerSetter.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject, useRef } from 'react' 2 | import { type CallbackSetter, type SomeCallback } from '../shared/types' 3 | 4 | /** 5 | * Returns an array where the first item is the [ref](https://reactjs.org/docs/hooks-reference.html#useref) to a 6 | * callback function and the second one is a reference to a function for can change the first ref. 7 | * 8 | * Although it looks quite similar to [useState](https://reactjs.org/docs/hooks-reference.html#usestate), 9 | * in this case the setter just makes sure the given callback is indeed a new function. 10 | * **Setting a callback ref does not force your component to re-render.** 11 | * 12 | * `createHandlerSetter` is meant to be used internally to abstracting other hooks. 13 | * Don't use this function to abstract hooks outside this library as it changes quite often 14 | */ 15 | const createHandlerSetter = (callback?: SomeCallback) => { 16 | const handlerRef = useRef(callback) 17 | 18 | const setHandler = useRef((nextCallback: SomeCallback) => { 19 | if (typeof nextCallback !== 'function') { 20 | throw new Error('the argument supplied to the \'setHandler\' function should be of type function') 21 | } 22 | 23 | handlerRef.current = nextCallback 24 | }) 25 | 26 | return [handlerRef, setHandler.current] as [RefObject>, CallbackSetter] 27 | } 28 | 29 | export default createHandlerSetter 30 | -------------------------------------------------------------------------------- /src/factory/createStorageHook.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | import safelyParseJson from '../shared/safelyParseJson' 3 | import isClient from '../shared/isClient' 4 | import isAPISupported from '../shared/isAPISupported' 5 | import isDevelopment from '../shared/isDevelopment' 6 | import noop from '../shared/noop' 7 | import warnOnce from '../shared/warnOnce' 8 | 9 | /** 10 | * A utility to quickly create hooks to access both Session Storage and Local Storage 11 | */ 12 | const createStorageHook = (type: 'session' | 'local') => { 13 | type SetValue = (value: TValue | ((previousValue: TValue) => TValue)) => void 14 | const storageName: `${typeof type}Storage` = `${type}Storage` 15 | 16 | if (isClient && !isAPISupported(storageName)) { 17 | warnOnce(`${storageName} is not supported`) 18 | } 19 | 20 | /** 21 | * the hook 22 | */ 23 | return function useStorageCreatedHook (storageKey: string, defaultValue?: any): [TValue | null, SetValue] { 24 | if (!isClient) { 25 | if (isDevelopment) { 26 | warnOnce(`Please be aware that ${storageName} could not be available during SSR`) 27 | } 28 | return [JSON.stringify(defaultValue) as unknown as TValue, noop] 29 | } 30 | const storage = (window)[storageName] 31 | 32 | const safelySetStorage = useCallback((valueToStore: string) => { 33 | try { 34 | storage.setItem(storageKey, valueToStore) 35 | // eslint-disable-next-line no-empty 36 | } catch (e) { 37 | } 38 | }, [storage, storageKey]) 39 | 40 | const [storedValue, setStoredValue] = useState( 41 | () => { 42 | let valueToStore: string 43 | try { 44 | valueToStore = storage.getItem(storageKey) ?? JSON.stringify(defaultValue) 45 | } catch (e) { 46 | valueToStore = JSON.stringify(defaultValue) 47 | } 48 | 49 | safelySetStorage(valueToStore) 50 | return safelyParseJson(valueToStore) 51 | } 52 | ) 53 | 54 | const setValue: SetValue = useCallback( 55 | (value: TValue) => { 56 | setStoredValue((current: TValue) => { 57 | const valueToStore = value instanceof Function ? value(current) : value 58 | safelySetStorage(JSON.stringify(valueToStore)) 59 | return valueToStore 60 | }) 61 | }, 62 | [safelySetStorage] 63 | ) 64 | 65 | return [storedValue, setValue] 66 | } 67 | } 68 | 69 | export default createStorageHook 70 | -------------------------------------------------------------------------------- /src/shared/geolocationUtils.ts: -------------------------------------------------------------------------------- 1 | import { type BRHGeolocationPosition } from './types' 2 | 3 | export const geoStandardOptions: PositionOptions = Object.freeze({ 4 | enableHighAccuracy: false, 5 | timeout: 0xFFFFFFFF, 6 | maximumAge: 0 7 | }) 8 | 9 | /** 10 | * Checks if two position are equals 11 | */ 12 | export const isSamePosition = (current: BRHGeolocationPosition, next: BRHGeolocationPosition): boolean => { 13 | if (!current || !next || !next.coords) return false 14 | if (current.timestamp && next.timestamp && current.timestamp !== next.timestamp) return false 15 | 16 | return ( 17 | (current.coords.latitude === next.coords.latitude) && 18 | (current.coords.longitude === next.coords.longitude) && 19 | (current.coords.altitude === next.coords.altitude) && 20 | (current.coords.accuracy === next.coords.accuracy) && 21 | (current.coords.altitudeAccuracy === next.coords.altitudeAccuracy) && 22 | (current.coords.heading === next.coords.heading) && 23 | (current.coords.speed === next.coords.speed) 24 | ) 25 | } 26 | 27 | /** 28 | * Given a position object returns only its properties 29 | */ 30 | export const makePositionObj = (position: BRHGeolocationPosition) => (!position 31 | ? null 32 | : ({ 33 | timestamp: position.timestamp, 34 | coords: { 35 | latitude: position.coords.latitude, 36 | longitude: position.coords.longitude, 37 | altitude: position.coords.altitude, 38 | accuracy: position.coords.accuracy, 39 | altitudeAccuracy: position.coords.altitudeAccuracy, 40 | heading: position.coords.heading, 41 | speed: position.coords.speed 42 | } 43 | })) 44 | -------------------------------------------------------------------------------- /src/shared/isAPISupported.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Exports a boolean value reporting whether the given API is supported or not 3 | */ 4 | const isApiSupported = (api: string): boolean => (typeof window !== 'undefined' ? api in window : false) 5 | 6 | export default isApiSupported 7 | -------------------------------------------------------------------------------- /src/shared/isClient.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Exports a boolean value reporting whether is client side or server side by checking on the window object 3 | */ 4 | const isClient = !!( 5 | typeof window !== 'undefined' && window.document && window.document.createElement 6 | ) 7 | 8 | export default isClient 9 | -------------------------------------------------------------------------------- /src/shared/isDevelopment.ts: -------------------------------------------------------------------------------- 1 | const isDevelopment = ( 2 | typeof process !== 'undefined' && typeof process.env !== 'undefined' && (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') 3 | ) 4 | 5 | export default isDevelopment 6 | -------------------------------------------------------------------------------- /src/shared/isFunction.ts: -------------------------------------------------------------------------------- 1 | type SomeFunction = (...args: any[]) => any 2 | 3 | const isFunction = (functionToCheck: unknown): functionToCheck is SomeFunction => ( 4 | typeof functionToCheck === 'function' && 5 | !!functionToCheck.constructor && 6 | !!functionToCheck.call && 7 | !!functionToCheck.apply 8 | ) 9 | 10 | export default isFunction 11 | -------------------------------------------------------------------------------- /src/shared/noop.ts: -------------------------------------------------------------------------------- 1 | import { type Noop } from './types' 2 | 3 | /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ 4 | const noop: Noop = (...args: any[]) => undefined 5 | 6 | noop.noop = true 7 | 8 | export default noop 9 | -------------------------------------------------------------------------------- /src/shared/safeHasOwnProperty.ts: -------------------------------------------------------------------------------- 1 | const safeHasOwnProperty = (obj: any, prop: string): boolean => (obj ? Object.prototype.hasOwnProperty.call(obj, prop) : false) 2 | 3 | export default safeHasOwnProperty 4 | -------------------------------------------------------------------------------- /src/shared/safelyParseJson.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Safely parse JSON string to object or null 3 | * @param parseString 4 | */ 5 | const safelyParseJson = (parseString: string): T | null => { 6 | try { 7 | return JSON.parse(parseString) 8 | } catch (e) { 9 | return null 10 | } 11 | } 12 | 13 | export default safelyParseJson 14 | -------------------------------------------------------------------------------- /src/shared/swipeUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Takes a mouse or a touch events and returns clientX and clientY values 3 | * @param event 4 | * @return {[undefined, undefined]} 5 | */ 6 | export const getPointerCoordinates = (event: TouchEvent | MouseEvent): [number, number] => { 7 | if ((event as TouchEvent).touches) { 8 | const { clientX, clientY } = (event as TouchEvent).touches[0] 9 | return [clientX, clientY] 10 | } 11 | 12 | const { clientX, clientY } = event as MouseEvent 13 | 14 | return [clientX, clientY] 15 | } 16 | 17 | export const getHorizontalDirection = (alpha: number) => (alpha < 0 ? 'right' : 'left') 18 | 19 | export const getVerticalDirection = (alpha: number) => (alpha < 0 ? 'down' : 'up') 20 | 21 | export type Direction = 'right' | 'left' | 'down' | 'up' 22 | 23 | export const getDirection = (currentPoint: [number, number], startingPoint: [number, number], alpha: [number, number]): Direction => { 24 | const alphaX = startingPoint[0] - currentPoint[0] 25 | const alphaY = startingPoint[1] - currentPoint[1] 26 | if (Math.abs(alphaX) > Math.abs(alphaY)) { 27 | return getHorizontalDirection(alpha[0]) 28 | } 29 | 30 | return getVerticalDirection(alpha[1]) 31 | } 32 | -------------------------------------------------------------------------------- /src/shared/types.ts: -------------------------------------------------------------------------------- 1 | export interface Noop { 2 | noop: true 3 | 4 | (...args: any[]): any 5 | } 6 | 7 | /** 8 | * Represent a generic function. 9 | * Used internally to improve code readability 10 | */ 11 | export type GenericFunction = (...args: any[]) => any 12 | 13 | /** 14 | * Typed generic callback function, used mostly internally 15 | * to defined callback setters 16 | */ 17 | export type SomeCallback = (...args: TArgs[]) => TResult 18 | 19 | /** 20 | * A callback setter is generally used to set the value of 21 | * a callback that will be used to perform updates 22 | */ 23 | export type CallbackSetter = (nextCallback: SomeCallback) => void 24 | 25 | /** 26 | * This type is used internally to avoid using directly GeolocationPosition 27 | * as that type is not always compatible with all typescript versions 28 | */ 29 | export interface BRHGeolocationPosition { 30 | readonly timestamp: number 31 | readonly coords: { 32 | readonly accuracy: number 33 | readonly altitude: number | null 34 | readonly altitudeAccuracy: number | null 35 | readonly heading: number | null 36 | readonly latitude: number 37 | readonly longitude: number 38 | readonly speed: number | null 39 | } 40 | } 41 | 42 | /** 43 | * This type is used internally to avoid using directly GeolocationPositionError 44 | * as that type is not always compatible with all typescript versions 45 | */ 46 | export interface BRHGeolocationPositionError { 47 | readonly code: number 48 | readonly message: string 49 | readonly PERMISSION_DENIED: number 50 | readonly POSITION_UNAVAILABLE: number 51 | readonly TIMEOUT: number 52 | } 53 | -------------------------------------------------------------------------------- /src/shared/warnOnce.ts: -------------------------------------------------------------------------------- 1 | const cache = new Map() 2 | 3 | /** 4 | * A tiny wrapper around console.warn that makes sure the message is only displayed once. 5 | * Used mainly to avoid polluting server side logs 6 | * @param message 7 | */ 8 | const warnOnce = (message: string) => { 9 | if (cache.has(message)) return 10 | 11 | cache.set(message, true) 12 | 13 | // eslint-disable-next-line no-console 14 | console.warn(message) 15 | } 16 | 17 | export default warnOnce 18 | -------------------------------------------------------------------------------- /src/useConditionalTimeout.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef, useState } from 'react' 2 | import isFunction from './shared/isFunction' 3 | import { type GenericFunction } from './shared/types' 4 | import usePreviousValue from './usePreviousValue' 5 | 6 | /** 7 | * An async-utility hook that accepts a callback function and a delay time (in milliseconds), then delays the 8 | * execution of the given function by the defined time from when the condition verifies. 9 | */ 10 | const useConditionalTimeout = 11 | (fn: TCallback, milliseconds: number, condition: boolean, options: UseConditionalTimeoutOptios = defaultOptions) => { 12 | const opts = { ...defaultOptions, ...(options || {}) } 13 | const timeout = useRef() 14 | const callback = useRef(fn) 15 | const [isCleared, setIsCleared] = useState(false) 16 | const prevCondition = usePreviousValue(condition) 17 | 18 | // the clear method 19 | const clear = useCallback(() => { 20 | if (timeout.current) { 21 | clearTimeout(timeout.current) 22 | setIsCleared(true) 23 | } 24 | }, []) 25 | 26 | // if the provided function changes, change its reference 27 | useEffect(() => { 28 | if (isFunction(fn)) { 29 | callback.current = fn 30 | } 31 | }, [fn]) 32 | 33 | // when the milliseconds change, reset the timeout 34 | useEffect(() => { 35 | if (condition && typeof milliseconds === 'number') { 36 | timeout.current = setTimeout(() => { 37 | callback.current() 38 | }, milliseconds) 39 | } 40 | }, [condition, milliseconds]) 41 | 42 | // when the condition change, clear the timeout 43 | useEffect(() => { 44 | if (prevCondition && condition !== prevCondition && opts.cancelOnConditionChange) { 45 | clear() 46 | } 47 | }, [condition, options]) 48 | 49 | // when component unmount clear the timeout 50 | useEffect(() => () => { 51 | if (opts.cancelOnUnmount) { 52 | clear() 53 | } 54 | }, []) 55 | 56 | return [isCleared, clear] as UseConditionalTimeoutReturn 57 | } 58 | 59 | export interface UseConditionalTimeoutOptios { 60 | cancelOnUnmount?: boolean 61 | cancelOnConditionChange?: boolean 62 | } 63 | 64 | const defaultOptions: UseConditionalTimeoutOptios = { 65 | cancelOnUnmount: true, 66 | cancelOnConditionChange: true 67 | } 68 | 69 | export type UseConditionalTimeoutReturn = [boolean, () => void] 70 | 71 | export default useConditionalTimeout 72 | -------------------------------------------------------------------------------- /src/useDarkMode.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import useMediaQuery from './useMediaQuery' 3 | import useUpdateEffect from './useUpdateEffect' 4 | import useLocalStorage from './useLocalStorage' 5 | import noop from './shared/noop' 6 | import isClient from './shared/isClient' 7 | import isDevelopment from './shared/isDevelopment' 8 | import warnOnce from './shared/warnOnce' 9 | 10 | const COLOR_SCHEME_QUERY = '(prefers-color-scheme: dark)' 11 | export const LOCAL_STORAGE_KEY = 'beautiful-react-hooks-is-dark-mode' 12 | 13 | const useDarkMode = (defaultValue?: boolean, localStorageKey: string = LOCAL_STORAGE_KEY) => { 14 | if (!isClient) { 15 | if (!isDevelopment) { 16 | warnOnce('Please be aware that useDarkMode hook could not be available during SSR') 17 | } 18 | 19 | return Object.freeze({ 20 | toggle: noop, 21 | enable: noop, 22 | disable: noop, 23 | isDarkMode: defaultValue ?? false 24 | }) 25 | } 26 | 27 | const isDarkOS = useMediaQuery(COLOR_SCHEME_QUERY) 28 | const [isDarkMode, setIsDarkMode] = useLocalStorage( 29 | localStorageKey, 30 | defaultValue ?? isDarkOS ?? false 31 | ) 32 | 33 | useUpdateEffect(() => { 34 | setIsDarkMode(isDarkOS) 35 | }, [isDarkOS]) 36 | 37 | const enable = useCallback(() => { 38 | setIsDarkMode(true) 39 | }, []) 40 | 41 | const disable = useCallback(() => { 42 | setIsDarkMode(false) 43 | }, []) 44 | 45 | const toggle = useCallback(() => { 46 | setIsDarkMode((prev) => !prev) 47 | }, [setIsDarkMode]) 48 | 49 | return Object.freeze({ 50 | toggle, 51 | enable, 52 | disable, 53 | isDarkMode: isDarkMode ?? false 54 | }) 55 | } 56 | 57 | export interface UseDarkModeReturn { 58 | isDarkMode: boolean 59 | toggle: () => void 60 | enable: () => void 61 | disable: () => void 62 | } 63 | 64 | export default useDarkMode 65 | -------------------------------------------------------------------------------- /src/useDebouncedCallback.ts: -------------------------------------------------------------------------------- 1 | import { type DependencyList, useCallback, useEffect, useRef } from 'react' 2 | import debounce from 'lodash.debounce' 3 | import { type GenericFunction } from './shared/types' 4 | import useWillUnmount from './useWillUnmount' 5 | 6 | export interface DebounceOptions { 7 | leading?: boolean | undefined 8 | maxWait?: number | undefined 9 | trailing?: boolean | undefined 10 | } 11 | 12 | const defaultOptions: DebounceOptions = { 13 | leading: false, 14 | trailing: true 15 | } 16 | 17 | /** 18 | * Accepts a function and returns a new debounced yet memoized version of that same function that delays 19 | * its invoking by the defined time. 20 | * If time is not defined, its default value will be 250ms. 21 | */ 22 | const useDebouncedCallback = 23 | (fn: TCallback, dependencies?: DependencyList, wait: number = 600, options: DebounceOptions = defaultOptions) => { 24 | const debounced = useRef(debounce(fn, wait, options)) 25 | 26 | useEffect(() => { 27 | debounced.current = debounce(fn, wait, options) 28 | }, [fn, wait, options]) 29 | 30 | useWillUnmount(() => { 31 | debounced.current?.cancel() 32 | }) 33 | 34 | return useCallback(debounced.current, dependencies ?? []) 35 | } 36 | 37 | export default useDebouncedCallback 38 | -------------------------------------------------------------------------------- /src/useDefaultedState.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | const maybeState = (state: TValue, defaultValue?: TValue) => (state ?? defaultValue) as TValue 4 | 5 | /** 6 | * Returns a safe state by making sure the given value is not null or undefined 7 | */ 8 | const useDefaultedState = (defaultValue: TValue, initialState?: TValue) => { 9 | const [state, setState] = useState(maybeState(initialState, defaultValue) as TValue) 10 | 11 | const setStateSafe = useCallback((nextState: TValue) => { 12 | setState(maybeState(nextState, defaultValue)) 13 | }, [setState]) 14 | 15 | return [maybeState(state, defaultValue), setStateSafe] as [TValue, typeof setStateSafe] 16 | } 17 | 18 | export default useDefaultedState 19 | -------------------------------------------------------------------------------- /src/useDidMount.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | import isFunction from './shared/isFunction' 3 | import { type GenericFunction, type Noop } from './shared/types' 4 | import createHandlerSetter from './factory/createHandlerSetter' 5 | 6 | /** 7 | * Returns a callback setter for a function to be performed when the component did mount. 8 | */ 9 | const useDidMount = (callback?: TCallback) => { 10 | const mountRef = useRef(false) 11 | const [handler, setHandler] = createHandlerSetter(callback) 12 | 13 | useEffect(() => { 14 | if (isFunction(handler?.current) && !mountRef.current) { 15 | handler.current() 16 | mountRef.current = true 17 | } 18 | }, []) 19 | 20 | return setHandler 21 | } 22 | 23 | export default useDidMount 24 | -------------------------------------------------------------------------------- /src/useDrag.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject, useState } from 'react' 2 | import useDragEvents from './useDragEvents' 3 | 4 | export interface UseDragOptions { 5 | dragImage?: string 6 | dragImageXOffset?: number 7 | dragImageYOffset?: number 8 | transfer?: string | number | Record 9 | transferFormat?: string 10 | } 11 | 12 | const defaultOptions: UseDragOptions = { 13 | dragImageXOffset: 0, 14 | dragImageYOffset: 0, 15 | transferFormat: 'text' 16 | } 17 | 18 | const useDrag = (targetRef: RefObject, options = defaultOptions) => { 19 | const { onDragStart, onDragEnd } = useDragEvents(targetRef, true) 20 | const [isDragging, setIsDragging] = useState(false) 21 | const opts: UseDragOptions = { ...defaultOptions, ...(options || {}) } 22 | 23 | onDragStart((event: DragEvent) => { 24 | setIsDragging(true) 25 | 26 | if (opts.dragImage && event.dataTransfer) { 27 | const img = new Image() 28 | img.src = opts.dragImage 29 | event.dataTransfer.setDragImage(img, opts.dragImageXOffset ?? 0, opts.dragImageYOffset ?? 0) 30 | } 31 | 32 | if (opts.transfer && event.dataTransfer) { 33 | const data = typeof opts.transfer === 'object' ? JSON.stringify(opts.transfer) : `${opts.transfer}` 34 | event.dataTransfer.setData(opts.transferFormat ?? 'text', data) 35 | } 36 | }) 37 | 38 | onDragEnd(() => { 39 | setIsDragging(false) 40 | }) 41 | 42 | return isDragging 43 | } 44 | 45 | export default useDrag 46 | -------------------------------------------------------------------------------- /src/useDragEvents.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject, useEffect } from 'react' 2 | import safeHasOwnProperty from './shared/safeHasOwnProperty' 3 | import useEvent from './useEvent' 4 | import { type CallbackSetter } from './shared/types' 5 | 6 | export interface UseDragEventsResult { 7 | onDrag: CallbackSetter 8 | onDrop: CallbackSetter 9 | onDragEnter: CallbackSetter 10 | onDragEnd: CallbackSetter 11 | onDragExit: CallbackSetter 12 | onDragLeave: CallbackSetter 13 | onDragOver: CallbackSetter 14 | onDragStart: CallbackSetter 15 | } 16 | 17 | /** 18 | * Returns an object of callback setters to handle the drag-related events. 19 | * It accepts a DOM ref representing the events target (where attach the events to). 20 | * 21 | * Returned callback setters: `onDrag`, `onDrop`, `onDragEnter`, `onDragEnd`, `onDragExit`, `onDragLeave`, 22 | * `onDragOver`, `onDragStart`; 23 | */ 24 | const useDragEvents = (targetRef: RefObject, isDraggable: boolean = true) => { 25 | const onDrag = useEvent(targetRef, 'drag') 26 | const onDrop = useEvent(targetRef, 'drop') 27 | const onDragEnter = useEvent(targetRef, 'dragenter') 28 | const onDragEnd = useEvent(targetRef, 'dragend') 29 | const onDragExit = useEvent(targetRef, 'dragexit') 30 | const onDragLeave = useEvent(targetRef, 'dragleave') 31 | const onDragOver = useEvent(targetRef, 'dragover') 32 | const onDragStart = useEvent(targetRef, 'dragstart') 33 | 34 | if (targetRef !== null && !safeHasOwnProperty(targetRef, 'current')) { 35 | throw new Error('Unable to assign any drag event to the given ref') 36 | } 37 | 38 | useEffect(() => { 39 | if (isDraggable && targetRef.current && !targetRef.current.hasAttribute('draggable')) { 40 | targetRef.current.setAttribute('draggable', String(true)) 41 | } 42 | }, []) 43 | 44 | return Object.freeze({ 45 | onDrag, 46 | onDrop, 47 | onDragEnter, 48 | onDragEnd, 49 | onDragExit, 50 | onDragLeave, 51 | onDragOver, 52 | onDragStart 53 | }) 54 | } 55 | 56 | export default useDragEvents 57 | -------------------------------------------------------------------------------- /src/useDropZone.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject, useState } from 'react' 2 | import useDragEvents from './useDragEvents' 3 | import { type CallbackSetter } from './shared/types' 4 | 5 | export interface UseDropZoneResult { 6 | readonly isOver: boolean 7 | readonly onDrop: CallbackSetter 8 | } 9 | 10 | const useDropZone = (targetRef: RefObject) => { 11 | const { onDrop, onDragOver, onDragLeave } = useDragEvents(targetRef, false) 12 | const [isOver, setIsOver] = useState(false) 13 | 14 | onDragOver((event: DragEvent) => { 15 | event.preventDefault() 16 | setIsOver(true) 17 | }) 18 | 19 | onDragLeave(() => { 20 | setIsOver(false) 21 | }) 22 | 23 | return Object.freeze({ 24 | isOver, 25 | onDrop 26 | }) 27 | } 28 | 29 | export default useDropZone 30 | -------------------------------------------------------------------------------- /src/useEvent.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject, useEffect } from 'react' 2 | import createHandlerSetter from './factory/createHandlerSetter' 3 | import safeHasOwnProperty from './shared/safeHasOwnProperty' 4 | 5 | /** 6 | * Accepts the reference to an HTML Element and an event name then performs the necessary operations to listen to the event 7 | * when fired from that HTML Element. 8 | */ 9 | const useEvent = 10 | (target: RefObject, eventName: string, options?: AddEventListenerOptions) => { 11 | const [handler, setHandler] = createHandlerSetter() 12 | 13 | if (!!target && !safeHasOwnProperty(target, 'current')) { 14 | throw new Error('Unable to assign any scroll event to the given ref') 15 | } 16 | 17 | useEffect(() => { 18 | const cb: EventListenerOrEventListenerObject = (event: TEvent) => { 19 | if (handler.current) { 20 | handler.current(event) 21 | } 22 | } 23 | 24 | if (target.current?.addEventListener && handler.current) { 25 | target.current.addEventListener(eventName, cb, options) 26 | } 27 | 28 | return () => { 29 | if (target.current?.addEventListener && handler.current) { 30 | target.current.removeEventListener(eventName, cb, options) 31 | } 32 | } 33 | }, [eventName, target.current, options]) 34 | 35 | return setHandler 36 | } 37 | 38 | export default useEvent 39 | -------------------------------------------------------------------------------- /src/useGeolocation.ts: -------------------------------------------------------------------------------- 1 | import useGeolocationState, { type UseGeolocationStateResult } from './useGeolocationState' 2 | import useGeolocationEvents, { type UseGeolocationEventsResult } from './useGeolocationEvents' 3 | import { geoStandardOptions } from './shared/geolocationUtils' 4 | 5 | /** 6 | * Returns an array where the first item is the geolocation state from the `useGeolocationState` hook and the 7 | * second one is the object of callback setters from the `useGeolocationEvents` hook. 8 | * It is intended as a shortcut to those hooks. 9 | */ 10 | const useGeolocation = (options: PositionOptions = geoStandardOptions) => { 11 | const state = useGeolocationState(options) 12 | const events = useGeolocationEvents(options) 13 | 14 | return [state, events] as [UseGeolocationStateResult, UseGeolocationEventsResult] 15 | } 16 | 17 | export default useGeolocation 18 | -------------------------------------------------------------------------------- /src/useGeolocationEvents.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useRef } from 'react' 2 | import createHandlerSetter from './factory/createHandlerSetter' 3 | import { geoStandardOptions } from './shared/geolocationUtils' 4 | import { type BRHGeolocationPosition, type BRHGeolocationPositionError } from './shared/types' 5 | 6 | export interface UseGeolocationEventsResult { 7 | isSupported: boolean 8 | onChange: (callback: (position: BRHGeolocationPosition) => void) => void 9 | onError: (callback: (error: BRHGeolocationPositionError) => void) => void 10 | } 11 | 12 | /** 13 | * Returns a frozen object of callback setters to handle the geolocation events. 14 | * So far, the supported methods are: `onChange`, invoked when the position changes and `onError`, invoked when 15 | * an error occur while retrieving the position. 16 | * The returned object also contains the `isSupported` boolean flag reporting whether the geolocation API is supported. 17 | */ 18 | const useGeolocationEvents = (options: PositionOptions = geoStandardOptions) => { 19 | const watchId = useRef() 20 | const [onChangeRef, setOnChangeRef] = createHandlerSetter() 21 | const [onErrorRef, setOnErrorRef] = createHandlerSetter() 22 | const isSupported = useMemo(() => typeof window !== 'undefined' && 'geolocation' in window.navigator, []) 23 | 24 | if (!isSupported) { 25 | throw new Error('The Geolocation API is not supported') 26 | } 27 | 28 | useEffect(() => { 29 | const onSuccess = (successEvent: BRHGeolocationPosition) => { 30 | if (onChangeRef.current) { 31 | onChangeRef.current(successEvent) 32 | } 33 | } 34 | const onError = (err: BRHGeolocationPositionError) => { 35 | if (onErrorRef.current) { 36 | onErrorRef.current(err) 37 | } 38 | } 39 | 40 | if (isSupported) { 41 | watchId.current = window.navigator.geolocation.watchPosition(onSuccess, onError, options) 42 | } 43 | 44 | return () => { 45 | if (isSupported && watchId.current) { 46 | window.navigator.geolocation.clearWatch(watchId.current) 47 | } 48 | } 49 | }, []) 50 | 51 | return Object.freeze({ 52 | isSupported, 53 | onChange: setOnChangeRef, 54 | onError: setOnErrorRef 55 | }) 56 | } 57 | 58 | export default useGeolocationEvents 59 | -------------------------------------------------------------------------------- /src/useGlobalEvent.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject } from 'react' 2 | import useEvent from './useEvent' 3 | import isClient from './shared/isClient' 4 | import { type CallbackSetter } from './shared/types' 5 | import noop from './shared/noop' 6 | 7 | /** 8 | * Accepts an event name then returns a callback setter for a function to be performed when the event triggers. 9 | */ 10 | const useGlobalEvent = (eventName: keyof WindowEventMap, opts?: AddEventListenerOptions) => { 11 | if (!isClient) { 12 | return noop as CallbackSetter 13 | } 14 | 15 | const target = { current: window } as unknown as RefObject // that's a bit of a hack but it works 16 | return useEvent(target, eventName, opts) 17 | } 18 | 19 | export default useGlobalEvent 20 | -------------------------------------------------------------------------------- /src/useHorizontalSwipe.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject } from 'react' 2 | import useSwipe, { type UseSwipeOptions } from './useSwipe' 3 | 4 | const defaultOptions: UseSwipeOptions = { 5 | threshold: 15, 6 | preventDefault: true 7 | } 8 | 9 | /** 10 | * A shortcut to useSwipe (with horizontal options) 11 | */ 12 | const useHorizontalSwipe = (ref?: RefObject, options: UseSwipeOptions = defaultOptions) => { 13 | const opts: UseSwipeOptions = { ...defaultOptions, ...(options || {}), ...{ direction: 'horizontal' } } 14 | 15 | return useSwipe(ref, opts) 16 | } 17 | 18 | export default useHorizontalSwipe 19 | -------------------------------------------------------------------------------- /src/useInfiniteScroll.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject, useRef } from 'react' 2 | import useEvent from './useEvent' 3 | import isFunction from './shared/isFunction' 4 | import safeHasOwnProperty from './shared/safeHasOwnProperty' 5 | import createHandlerSetter from './factory/createHandlerSetter' 6 | 7 | /** 8 | * Accepts an HTML Element ref, then returns a function that allows you to handle the infinite 9 | * scroll for that specific element. 10 | */ 11 | const useInfiniteScroll = (ref: RefObject, delay = 300) => { 12 | const onScroll = useEvent(ref, 'scroll', { passive: true }) 13 | const [onScrollEnd, setOnScrollEnd] = createHandlerSetter() 14 | const timeoutRef = useRef() 15 | 16 | if (ref && !safeHasOwnProperty(ref, 'current')) { 17 | throw new Error('Unable to assign any scroll event to the given ref') 18 | } 19 | 20 | onScroll((event: UIEvent) => { 21 | const { target } = event 22 | const el = target as HTMLDivElement 23 | if (el) { 24 | const isBottom = Math.abs(el.scrollHeight - el.clientHeight - el.scrollTop) < 1 25 | 26 | // event.preventDefault() 27 | event.stopPropagation() 28 | 29 | if (isBottom && isFunction(onScrollEnd?.current)) { 30 | clearTimeout(timeoutRef.current) 31 | 32 | timeoutRef.current = setTimeout(() => { 33 | if (onScrollEnd.current && isFunction(onScrollEnd.current)) { 34 | onScrollEnd.current() 35 | } 36 | clearTimeout(timeoutRef.current) 37 | }, delay) 38 | } 39 | } 40 | }) 41 | 42 | return setOnScrollEnd 43 | } 44 | 45 | export default useInfiniteScroll 46 | -------------------------------------------------------------------------------- /src/useInterval.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef, useState } from 'react' 2 | 3 | import isFunction from './shared/isFunction' 4 | import { type GenericFunction } from './shared/types' 5 | 6 | export interface UseIntervalOptions { 7 | cancelOnUnmount?: boolean 8 | } 9 | 10 | const defaultOptions: UseIntervalOptions = { 11 | cancelOnUnmount: true 12 | } 13 | 14 | /** 15 | * An async-utility hook that accepts a callback function and a delay time (in milliseconds), then repeats the 16 | * execution of the given function by the defined milliseconds. 17 | */ 18 | const useInterval = 19 | (fn: TCallback, milliseconds: number, options: UseIntervalOptions = defaultOptions) => { 20 | const opts = { ...defaultOptions, ...(options || {}) } 21 | const timeout = useRef() 22 | const callback = useRef(fn) 23 | const [isCleared, setIsCleared] = useState(false) 24 | 25 | // the clear method 26 | const clear = useCallback(() => { 27 | if (timeout.current) { 28 | setIsCleared(true) 29 | clearInterval(timeout.current) 30 | } 31 | }, []) 32 | 33 | // if the provided function changes, change its reference 34 | useEffect(() => { 35 | if (isFunction(fn)) { 36 | callback.current = fn 37 | } 38 | }, [fn]) 39 | 40 | // when the milliseconds change, reset the timeout 41 | useEffect(() => { 42 | if (typeof milliseconds === 'number') { 43 | timeout.current = setInterval(() => { 44 | callback.current() 45 | }, milliseconds) 46 | } 47 | 48 | // cleanup previous interval 49 | return clear 50 | }, [milliseconds]) 51 | 52 | // when component unmount clear the timeout 53 | useEffect(() => () => { 54 | if (opts.cancelOnUnmount) { 55 | clear() 56 | } 57 | }, []) 58 | 59 | return [isCleared, clear] as [boolean, () => void] 60 | } 61 | 62 | export default useInterval 63 | -------------------------------------------------------------------------------- /src/useIsFirstRender.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | 3 | const useIsFirstRender = () => { 4 | const isFirstRenderRef = useRef(true) 5 | 6 | if (isFirstRenderRef.current) { 7 | isFirstRenderRef.current = false 8 | 9 | return true 10 | } 11 | 12 | return isFirstRenderRef.current 13 | } 14 | 15 | export default useIsFirstRender 16 | -------------------------------------------------------------------------------- /src/useLifecycle.ts: -------------------------------------------------------------------------------- 1 | import useDidMount from './useDidMount' 2 | import useWillUnmount from './useWillUnmount' 3 | import { type GenericFunction } from './shared/types' 4 | 5 | /** 6 | * Returns an object wrapping lifecycle hooks such as `useDidMount` or `useWillUnmount`. 7 | * It is intended as a shortcut to those hooks. 8 | */ 9 | const useLifecycle = 10 | (mount?: TMount, unmount?: TUnmount) => { 11 | const onDidMount = useDidMount(mount) 12 | const onWillUnmount = useWillUnmount(unmount) 13 | 14 | return { onDidMount, onWillUnmount } 15 | } 16 | 17 | export default useLifecycle 18 | -------------------------------------------------------------------------------- /src/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import createStorageHook from './factory/createStorageHook' 2 | 3 | /** 4 | * Save a value on local storage 5 | */ 6 | const useLocalStorage = createStorageHook('local') 7 | 8 | export default useLocalStorage 9 | -------------------------------------------------------------------------------- /src/useLongPress.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject, useCallback, useState } from 'react' 2 | import useMouseEvents from './useMouseEvents' 3 | import useConditionalTimeout from './useConditionalTimeout' 4 | import createHandlerSetter from './factory/createHandlerSetter' 5 | import useTouchEvents from './useTouchEvents' 6 | import { type CallbackSetter } from './shared/types' 7 | 8 | /** 9 | * A hook that facilitates the implementation of the long press functionality on a given target, supporting both mouse and touch events. 10 | */ 11 | const useLongPress = (target: RefObject, duration = 500) => { 12 | const { onMouseDown, onMouseUp, onMouseLeave } = useMouseEvents(target, false) 13 | const { onTouchStart, onTouchEnd } = useTouchEvents(target, false) 14 | const [isLongPressing, setIsLongPressing] = useState(false) 15 | const [timerOn, startTimer] = useState(false) 16 | const [onLongPressStart, setOnLongPressStart] = createHandlerSetter() 17 | const [onLongPressEnd, setOnLongPressEnd] = createHandlerSetter() 18 | 19 | const longPressStart = useCallback((event: MouseEvent | TouchEvent) => { 20 | event.preventDefault() 21 | startTimer(true) 22 | }, []) 23 | 24 | const longPressStop = useCallback((event: MouseEvent | TouchEvent) => { 25 | if (!isLongPressing) return 26 | clearTimeout() 27 | setIsLongPressing(false) 28 | startTimer(false) 29 | event.preventDefault() 30 | 31 | if (onLongPressEnd?.current) { 32 | onLongPressEnd.current() 33 | } 34 | }, [isLongPressing]) 35 | 36 | const [, clearTimeout] = useConditionalTimeout(() => { 37 | setIsLongPressing(true) 38 | 39 | if (onLongPressStart?.current) { 40 | onLongPressStart.current() 41 | } 42 | }, duration, timerOn) 43 | 44 | onMouseDown(longPressStart) 45 | onMouseLeave(longPressStop) 46 | onMouseUp(longPressStop) 47 | 48 | onTouchStart(longPressStart) 49 | onTouchEnd(longPressStop) 50 | 51 | return Object.freeze({ 52 | isLongPressing, 53 | onLongPressStart: setOnLongPressStart, 54 | onLongPressEnd: setOnLongPressEnd 55 | }) 56 | } 57 | 58 | export interface UseLongPressResult { 59 | isLongPressing: boolean 60 | onLongPressStart: CallbackSetter 61 | onLongPressEnd: CallbackSetter 62 | } 63 | 64 | export default useLongPress 65 | -------------------------------------------------------------------------------- /src/useMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import isClient from './shared/isClient' 3 | import isAPISupported from './shared/isAPISupported' 4 | import warnOnce from './shared/warnOnce' 5 | 6 | const errorMessage = 'matchMedia is not supported, this could happen both because window.matchMedia is not supported by' + 7 | ' your current browser or you\'re using the useMediaQuery hook whilst server side rendering.' 8 | 9 | /** 10 | * Accepts a media query string then uses the 11 | * [window.matchMedia](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) API to determine if it 12 | * matches with the current document. 13 | * It also monitor the document changes to detect when it matches or stops matching the media query. 14 | * Returns the validity state of the given media query. 15 | * 16 | */ 17 | const useMediaQuery = (mediaQuery: string) => { 18 | if (!isClient || !isAPISupported('matchMedia')) { 19 | warnOnce(errorMessage) 20 | return false 21 | } 22 | 23 | const [isVerified, setIsVerified] = useState(!!window.matchMedia(mediaQuery).matches) 24 | 25 | useEffect(() => { 26 | const mediaQueryList = window.matchMedia(mediaQuery) 27 | const documentChangeHandler = () => { setIsVerified(!!mediaQueryList.matches) } 28 | 29 | try { 30 | mediaQueryList.addEventListener('change', documentChangeHandler) 31 | } catch (e) { 32 | // Safari isn't supporting mediaQueryList.addEventListener 33 | mediaQueryList.addListener(documentChangeHandler) 34 | } 35 | 36 | documentChangeHandler() 37 | return () => { 38 | try { 39 | mediaQueryList.removeEventListener('change', documentChangeHandler) 40 | } catch (e) { 41 | // Safari isn't supporting mediaQueryList.removeEventListener 42 | mediaQueryList.removeListener(documentChangeHandler) 43 | } 44 | } 45 | }, [mediaQuery]) 46 | 47 | return isVerified 48 | } 49 | 50 | export default useMediaQuery 51 | -------------------------------------------------------------------------------- /src/useMouse.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject } from 'react' 2 | import useMouseEvents from './useMouseEvents' 3 | import useMouseState from './useMouseState' 4 | 5 | /** 6 | * Returns an array where the first item is the mouse state from the `useMouseState` hook and the second item 7 | * is the object of callback setters from the `useMouseEvents` hook. 8 | * It is intended as a shortcut to those hooks. 9 | */ 10 | const useMouse = (targetRef?: RefObject) => { 11 | const state = useMouseState(targetRef) 12 | const events = useMouseEvents(targetRef) 13 | 14 | return [state, events] 15 | } 16 | 17 | export default useMouse 18 | -------------------------------------------------------------------------------- /src/useMouseEvents.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject } from 'react' 2 | import useEvent from './useEvent' 3 | 4 | /** 5 | * Returns a frozen object of callback setters to handle the mouse events. 6 | * It accepts a DOM ref representing the events target. 7 | * If a target is not provided the events will be globally attached to the document object. 8 | * 9 | * ### Shall the `useMouseEvents` callbacks replace the standard mouse handler props? 10 | * 11 | * **They shall not!** 12 | * **useMouseEvents is meant to be used to abstract more complex hooks that need to control mouse**, for instance: 13 | * a drag n drop hook. 14 | * Using useMouseEvents handlers instead of the classic props approach it's just as bad as it sounds since you'll 15 | * lose the React SyntheticEvent performance boost. 16 | * If you were doing something like the following: 17 | */ 18 | const useMouseEvents = (targetRef?: RefObject, passive?: boolean) => { 19 | const target = targetRef ?? { current: window.document } as unknown as RefObject 20 | const onMouseDown = useEvent(target, 'mousedown', { passive }) 21 | const onMouseEnter = useEvent(target, 'mouseenter', { passive }) 22 | const onMouseLeave = useEvent(target, 'mouseleave', { passive }) 23 | const onMouseMove = useEvent(target, 'mousemove', { passive }) 24 | const onMouseOut = useEvent(target, 'mouseout', { passive }) 25 | const onMouseOver = useEvent(target, 'mouseover', { passive }) 26 | const onMouseUp = useEvent(target, 'mouseup', { passive }) 27 | 28 | return Object.freeze({ 29 | onMouseDown, 30 | onMouseEnter, 31 | onMouseLeave, 32 | onMouseMove, 33 | onMouseOut, 34 | onMouseOver, 35 | onMouseUp 36 | }) 37 | } 38 | 39 | export default useMouseEvents 40 | -------------------------------------------------------------------------------- /src/useMouseState.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject, useState } from 'react' 2 | import useMouseEvents from './useMouseEvents' 3 | 4 | const createStateObject = (event: MouseEvent) => ({ 5 | clientX: event.clientX, 6 | clientY: event.clientY, 7 | screenX: event.screenX, 8 | screenY: event.screenY 9 | }) 10 | 11 | /** 12 | * Returns the current state (position) of the mouse pointer. 13 | * It possibly accepts a DOM ref representing the mouse target. 14 | * If a target is not provided the state will be caught globally. 15 | */ 16 | const useMouseState = (targetRef?: RefObject) => { 17 | const [state, setState] = useState({ clientX: 0, clientY: 0, screenX: 0, screenY: 0 }) 18 | const { onMouseMove } = useMouseEvents(targetRef) 19 | 20 | onMouseMove((event: MouseEvent) => { 21 | const nextState = createStateObject(event) 22 | setState(nextState) 23 | }) 24 | 25 | return state 26 | } 27 | 28 | export default useMouseState 29 | -------------------------------------------------------------------------------- /src/useMutableState.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from 'react' 2 | 3 | /** 4 | * Returns a reactive value that can be used as a state. 5 | */ 6 | const useMutableState = >(initialState: TProxied) => { 7 | if (typeof initialState !== 'object' || initialState === null) throw new Error('The initial state must be an object') 8 | 9 | const [, setState] = useState(0) 10 | 11 | return useMemo(() => new Proxy(initialState, { 12 | set: (target, prop: keyof TProxied, value: TProxied[keyof TProxied]) => { 13 | if (target && target[prop] !== value) { 14 | target[prop] = value 15 | setState((state) => (state + 1)) 16 | } 17 | return true 18 | } 19 | }), []) 20 | } 21 | 22 | export default useMutableState 23 | -------------------------------------------------------------------------------- /src/useMutationObserver.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject, useEffect } from 'react' 2 | 3 | import isClient from './shared/isClient' 4 | import isApiSupported from './shared/isAPISupported' 5 | import warnOnce from './shared/warnOnce' 6 | 7 | // eslint-disable-next-line max-len 8 | const errorMessage = 'MutationObserver is not supported, this could happen both because window. MutationObserver is not supported by your current browser or you\'re using the useMutationObserver hook whilst server side rendering.' 9 | 10 | const defaultOptions: MutationObserverInit = { 11 | attributes: true, 12 | characterData: true, 13 | childList: true, 14 | subtree: true 15 | } 16 | 17 | const useMutationObserver = ( 18 | ref: RefObject, 19 | callback: MutationCallback, 20 | options: MutationObserverInit = defaultOptions 21 | ) => { 22 | const isSupported = isClient && isApiSupported('MutationObserver') 23 | 24 | if (!isSupported) { 25 | warnOnce(errorMessage) 26 | return 27 | } 28 | 29 | // eslint-disable-next-line consistent-return 30 | useEffect(() => { 31 | if (ref.current) { 32 | const observer = new MutationObserver(callback) 33 | observer.observe(ref.current, options) 34 | 35 | return () => { observer.disconnect() } 36 | } 37 | }, [callback, options]) 38 | } 39 | 40 | export default useMutationObserver 41 | -------------------------------------------------------------------------------- /src/useObjectState.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useReducer } from 'react' 2 | 3 | const reducer = ( 4 | previousState: TState, 5 | updatedState: Partial 6 | ) => ({ 7 | ...previousState, 8 | ...updatedState 9 | }) 10 | 11 | const useObjectState = ( 12 | initialState: TState 13 | ): [TState, (state: Partial) => void] => { 14 | const [state, dispatch] = useReducer( 15 | (previousState: TState, updatedState: Partial) => reducer(previousState, updatedState), 16 | initialState 17 | ) 18 | 19 | const setState = useCallback((updatedState: Partial): void => { dispatch(updatedState) }, [dispatch]) 20 | 21 | return [state, setState] 22 | } 23 | 24 | export default useObjectState 25 | -------------------------------------------------------------------------------- /src/useObservable.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { type Observable, type Observer } from 'rxjs' 3 | 4 | /** 5 | * Hook, which helps you combine rxjs flow and setState in your component 6 | */ 7 | const useObservable = > | ((value: T) => void)>(observable: Observable, setter: F) => { 8 | useEffect(() => { 9 | const subscription = observable.subscribe(setter) 10 | 11 | return () => { 12 | subscription.unsubscribe() 13 | } 14 | }, [observable, setter]) 15 | } 16 | 17 | export default useObservable 18 | -------------------------------------------------------------------------------- /src/useOnlineState.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import useGlobalEvent from './useGlobalEvent' 3 | import warnOnce from './shared/warnOnce' 4 | 5 | /** 6 | * Uses the [Navigator online API](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine) to define 7 | * whether the browser is connected or not. 8 | */ 9 | const useOnlineState = () => { 10 | /** 11 | * If the browser doesn't support the `navigator.onLine` state, the hook will always return true 12 | * assuming the app is already online. 13 | */ 14 | const isSupported = typeof window !== 'undefined' && 'ononline' in window 15 | const [isOnline, setIsOnline] = useState(isSupported ? navigator.onLine : true) 16 | const whenOnline = useGlobalEvent('online', { capture: true }) 17 | const whenOffline = useGlobalEvent('offline', { capture: true }) 18 | 19 | if (!isSupported) { 20 | warnOnce('The current device does not support the \'online/offline\' events, you should avoid using useOnlineState') 21 | return isOnline 22 | } 23 | 24 | whenOnline(() => { 25 | setIsOnline(true) 26 | }) 27 | 28 | whenOffline(() => { 29 | setIsOnline(false) 30 | }) 31 | 32 | return isOnline 33 | } 34 | 35 | export default useOnlineState 36 | -------------------------------------------------------------------------------- /src/usePreviousValue.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | /** 4 | * On each render returns the previous value of the given variable/constant. 5 | */ 6 | const usePreviousValue = (value?: TValue): TValue | undefined => { 7 | const prevValue = useRef() 8 | 9 | useEffect(() => { 10 | prevValue.current = value 11 | 12 | return () => { 13 | prevValue.current = undefined 14 | } 15 | }) 16 | 17 | return prevValue.current 18 | } 19 | 20 | export default usePreviousValue 21 | -------------------------------------------------------------------------------- /src/useQueryParam.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from 'react' 2 | import { useHistory } from 'react-router-dom' 3 | import useDidMount from './useDidMount' 4 | import useURLSearchParams from './useURLSearchParams' 5 | 6 | export interface UseQueryParamOptions { 7 | initialValue?: TValue 8 | replaceState?: boolean 9 | } 10 | 11 | /** 12 | * Ease the process of modify the query string in the URL for the current location. 13 | */ 14 | const useQueryParam = (key: string, options: UseQueryParamOptions = {}) => { 15 | const history = useHistory() 16 | const params = useURLSearchParams() 17 | const initialisedRef = useRef(false) 18 | const onMount = useDidMount() 19 | 20 | const setParam = useCallback((nextValue?: TValue) => { 21 | if (!nextValue) { 22 | params.delete(key) 23 | } else { 24 | params.set(key, nextValue) 25 | } 26 | 27 | if (options.replaceState) { 28 | history.replace({ search: params.toString() }) 29 | return 30 | } 31 | 32 | history.push({ search: params.toString() }) 33 | }, [options.replaceState, history]) 34 | 35 | onMount(() => { 36 | if (!params.has(key)) { 37 | initialisedRef.current = true 38 | setParam(options.initialValue) 39 | } 40 | }) 41 | 42 | return [initialisedRef.current ? params.get(key) : options.initialValue, setParam] as [TValue, (nextValue?: TValue) => void] 43 | } 44 | 45 | export default useQueryParam 46 | -------------------------------------------------------------------------------- /src/useQueryParams.ts: -------------------------------------------------------------------------------- 1 | import { useHistory } from 'react-router-dom' 2 | import { useCallback, useRef } from 'react' 3 | import useDidMount from './useDidMount' 4 | import useURLSearchParams from './useURLSearchParams' 5 | 6 | export interface UseQueryParamsOptions { 7 | initialValue?: TValue 8 | replaceState?: boolean 9 | } 10 | 11 | /** 12 | * Very similar to `useQueryParams`, it eases the process of manipulate a query string that handles multiple values 13 | */ 14 | const useQueryParams = (key: string, options: UseQueryParamsOptions = {}) => { 15 | const history = useHistory() 16 | const params = useURLSearchParams() 17 | const initialisedRef = useRef(false) 18 | const onMount = useDidMount() 19 | 20 | const setParam = useCallback((nextValue?: TValue) => { 21 | params.delete(key) 22 | 23 | if (nextValue) { 24 | nextValue.forEach((value) => { 25 | params.append(key, value) 26 | }) 27 | } 28 | 29 | if (options.replaceState) { 30 | history.replace({ search: params.toString() }) 31 | return 32 | } 33 | 34 | history.push({ search: params.toString() }) 35 | }, [options.replaceState, history]) 36 | 37 | onMount(() => { 38 | if (!params.has(key)) { 39 | setParam(options.initialValue) 40 | initialisedRef.current = true 41 | } 42 | }) 43 | 44 | return [initialisedRef.current ? params.getAll(key) : options.initialValue, setParam] as [TValue, (nextValue?: TValue) => void] 45 | } 46 | 47 | export default useQueryParams 48 | -------------------------------------------------------------------------------- /src/useRenderInfo.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | 3 | export interface RenderInfo { 4 | readonly module: string 5 | renders: number 6 | timestamp: null | number 7 | sinceLast: null | number | '[now]' 8 | } 9 | 10 | const getInitial = (moduleName: string): RenderInfo => ({ 11 | module: moduleName, 12 | renders: 0, 13 | timestamp: null, 14 | sinceLast: null 15 | }) 16 | 17 | /** 18 | * useRenderInfo 19 | * @param moduleName 20 | * @param log 21 | * @returns {{renders: number, module: *, timestamp: null}} 22 | */ 23 | const useRenderInfo = (moduleName: string = 'Unknown component', log: boolean = true) => { 24 | const { current: info } = useRef(getInitial(moduleName)) 25 | const now = +Date.now() 26 | 27 | info.renders += 1 28 | info.sinceLast = info.timestamp ? (now - info.timestamp) / 1000 : '[now]' 29 | info.timestamp = now 30 | 31 | if (log) { 32 | console.group(`${moduleName} info`) 33 | console.log(`Render no: ${info.renders}${info.renders > 1 ? `, ${info.sinceLast}s since last render` : ''}`) 34 | console.dir(info) 35 | console.groupEnd() 36 | } 37 | 38 | return info 39 | } 40 | 41 | export default useRenderInfo 42 | -------------------------------------------------------------------------------- /src/useRequestAnimationFrame.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from 'react' 2 | import createHandlerSetter from './factory/createHandlerSetter' 3 | import isClient from './shared/isClient' 4 | import isAPISupported from './shared/isAPISupported' 5 | import { type CallbackSetter, type GenericFunction } from './shared/types' 6 | import noop from './shared/noop' 7 | import warnOnce from './shared/warnOnce' 8 | 9 | export interface UseRequestAnimationFrameOpts { 10 | increment?: number 11 | startAt?: number 12 | finishAt?: number 13 | } 14 | 15 | const defaultOptions = { increment: 1, startAt: 0, finishAt: 100 } 16 | 17 | const errorMessage = 'requestAnimationFrame is not supported, this could happen both because ' + 18 | 'window.requestAnimationFrame is not supported by your current browser version or you\'re using the ' + 19 | 'useRequestAnimationFrame hook whilst server side rendering.' 20 | 21 | /** 22 | * Takes care of running an animating function, provided as the first argument, while keeping track of its progress. 23 | */ 24 | const useRequestAnimationFrame = (func: T, options: UseRequestAnimationFrameOpts = defaultOptions) => { 25 | if (!isClient || !isAPISupported('requestAnimationFrame')) { 26 | warnOnce(errorMessage) 27 | return noop as CallbackSetter 28 | } 29 | 30 | const opts = { ...defaultOptions, ...options } 31 | const progress = useRef(opts.startAt) 32 | const [onFinish, setOnFinish] = createHandlerSetter() 33 | 34 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 35 | const next = () => window.requestAnimationFrame(step) 36 | 37 | const step = useCallback(() => { 38 | if (progress.current <= opts.finishAt || opts.finishAt === -1) { 39 | func(progress.current, next) 40 | progress.current += opts.increment 41 | } else if (onFinish.current) { 42 | onFinish.current() 43 | } 44 | }, [func, opts.finishAt, opts.increment, progress.current, onFinish]) 45 | 46 | if (progress.current <= opts.startAt) { 47 | next() 48 | } 49 | 50 | return setOnFinish 51 | } 52 | 53 | export default useRequestAnimationFrame 54 | -------------------------------------------------------------------------------- /src/useResizeObserver.ts: -------------------------------------------------------------------------------- 1 | import debounce from 'lodash.debounce' 2 | import { type RefObject, useEffect, useRef, useState } from 'react' 3 | 4 | import isClient from './shared/isClient' 5 | import isFunction from './shared/isFunction' 6 | import isApiSupported from './shared/isAPISupported' 7 | import warnOnce from './shared/warnOnce' 8 | 9 | // eslint-disable-next-line max-len 10 | const errorMessage = 'ResizeObserver is not supported, this could happen both because window. ResizeObserver is not supported by your current browser or you\'re using the useResizeObserver hook whilst server side rendering.' 11 | 12 | export type DOMRectValues = Pick 13 | 14 | /** 15 | * Uses the ResizeObserver API to observe changes within the given HTML Element DOM Rect. 16 | * @param elementRef 17 | * @param debounceTimeout 18 | * @returns {undefined} 19 | */ 20 | const useResizeObserver = 21 | (elementRef: RefObject, debounceTimeout: number = 100): DOMRectValues | undefined => { 22 | const isSupported = isApiSupported('ResizeObserver') 23 | const observerRef = useRef(null) 24 | const [DOMRect, setDOMRect] = useState() 25 | 26 | if (isClient && !isSupported) { 27 | warnOnce(errorMessage) 28 | return undefined 29 | } 30 | 31 | // creates the observer reference on mount 32 | useEffect(() => { 33 | if (isSupported) { 34 | const fn = debounce((entries) => { 35 | const { bottom, height, left, right, top, width } = entries[0].contentRect 36 | 37 | setDOMRect({ bottom, height, left, right, top, width }) 38 | }, debounceTimeout) 39 | 40 | observerRef.current = new ResizeObserver(fn) 41 | 42 | return () => { 43 | fn.cancel() 44 | if (observerRef.current && isFunction(observerRef?.current?.disconnect)) { 45 | observerRef.current.disconnect() 46 | } 47 | } 48 | } 49 | 50 | return () => { 51 | } 52 | }, []) 53 | 54 | // observes on the provided element ref 55 | useEffect(() => { 56 | if (isSupported && elementRef.current) { 57 | if (observerRef.current && isFunction(observerRef?.current?.observe)) { 58 | observerRef.current.observe(elementRef.current) 59 | } 60 | } 61 | }, [elementRef.current]) 62 | 63 | return DOMRect 64 | } 65 | 66 | export default useResizeObserver 67 | -------------------------------------------------------------------------------- /src/useSearchQuery.ts: -------------------------------------------------------------------------------- 1 | import useQueryParam from './useQueryParam' 2 | 3 | /** 4 | * Ease the process of modify the 'search' query string in the URL for the current location. 5 | * It's just a shortcut/wrapper around useQueryParam 6 | */ 7 | const useSearchQuery = (initialValue?: TSearchKey, replaceState = false) => useQueryParam('search', { 8 | initialValue, 9 | replaceState 10 | }) 11 | 12 | export default useSearchQuery 13 | -------------------------------------------------------------------------------- /src/useSessionStorage.ts: -------------------------------------------------------------------------------- 1 | import createStorageHook from './factory/createStorageHook' 2 | 3 | /** 4 | * Save a value on session storage 5 | */ 6 | const useSessionStorage = createStorageHook('session') 7 | 8 | export default useSessionStorage 9 | -------------------------------------------------------------------------------- /src/useSpeechSynthesis.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useMemo } from 'react' 2 | 3 | /** 4 | * The options that can be passed to the hook 5 | * @see https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance 6 | */ 7 | export interface UseSpeechSynthesisOptions { 8 | rate?: number 9 | pitch?: number 10 | volume?: number 11 | voice?: SpeechSynthesisVoice 12 | } 13 | 14 | /** 15 | * The result of the hook 16 | */ 17 | export interface SpeechSynthesisResult { 18 | readonly speak: () => void 19 | readonly speechSynthUtterance: SpeechSynthesisUtterance 20 | } 21 | 22 | const defaultOptions: UseSpeechSynthesisOptions = { rate: 1, pitch: 1, volume: 1 } 23 | 24 | /** 25 | * Enables the possibility to perform a text-to-speech (with different voices) operation in your 26 | * React component by using the Web_Speech_API 27 | */ 28 | const useSpeechSynthesis = (text: string, options: UseSpeechSynthesisOptions = defaultOptions) => { 29 | const utter: SpeechSynthesisUtterance = useMemo(() => new SpeechSynthesisUtterance(text), [text]) 30 | const voiceOptions = { ...defaultOptions, ...options } 31 | utter.voice = voiceOptions.voice! 32 | 33 | useEffect(() => { 34 | utter.pitch = voiceOptions.pitch! 35 | }, [voiceOptions.pitch]) 36 | 37 | useEffect(() => { 38 | utter.rate = voiceOptions.rate! 39 | }, [voiceOptions.rate]) 40 | 41 | useEffect(() => { 42 | utter.volume = voiceOptions.volume! 43 | }, [voiceOptions.volume]) 44 | 45 | const speak = useCallback( 46 | () => { 47 | speechSynthesis.speak(utter) 48 | }, 49 | [text, voiceOptions.pitch, voiceOptions.rate, voiceOptions.voice, voiceOptions.volume] 50 | ) 51 | 52 | return Object.freeze({ 53 | speak, 54 | speechSynthUtterance: utter 55 | }) 56 | } 57 | 58 | export default useSpeechSynthesis 59 | -------------------------------------------------------------------------------- /src/useSystemVoices.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns all the available voices on the system. 3 | * This hook is here to backward compatibility with the previous version of the library that was using 4 | * a different non-stable version of the Web Speech API. 5 | */ 6 | const useSystemVoices = () => window.speechSynthesis.getVoices() 7 | 8 | export default useSystemVoices 9 | -------------------------------------------------------------------------------- /src/useThrottledCallback.ts: -------------------------------------------------------------------------------- 1 | import { type DependencyList, useCallback, useEffect, useRef } from 'react' 2 | import throttle from 'lodash.throttle' 3 | import { type GenericFunction } from './shared/types' 4 | import useWillUnmount from './useWillUnmount' 5 | 6 | interface ThrottleSettings { 7 | leading?: boolean | undefined 8 | trailing?: boolean | undefined 9 | } 10 | 11 | const defaultOptions: ThrottleSettings = { 12 | leading: false, 13 | trailing: true 14 | } 15 | 16 | /** 17 | * Accepts a function and returns a new throttled yet memoized version of that same function that waits the defined time 18 | * before allowing the next execution. 19 | * If time is not defined, its default value will be 250ms. 20 | */ 21 | const useThrottledCallback = 22 | (fn: TCallback, dependencies?: DependencyList, wait: number = 600, options: ThrottleSettings = defaultOptions) => { 23 | const throttled = useRef(throttle(fn, wait, options)) 24 | 25 | useEffect(() => { 26 | throttled.current = throttle(fn, wait, options) 27 | }, [fn, wait, options]) 28 | 29 | useWillUnmount(() => { 30 | throttled.current?.cancel() 31 | }) 32 | 33 | return useCallback(throttled.current, dependencies ?? []) 34 | } 35 | 36 | export default useThrottledCallback 37 | -------------------------------------------------------------------------------- /src/useTimeout.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef, useState } from 'react' 2 | 3 | import isFunction from './shared/isFunction' 4 | import { type GenericFunction } from './shared/types' 5 | 6 | export interface UseTimeoutOptions { 7 | cancelOnUnmount?: boolean 8 | } 9 | 10 | const defaultOptions = { 11 | cancelOnUnmount: true 12 | } 13 | 14 | /** 15 | * An async-utility hook that accepts a callback function and a delay time (in milliseconds), then delays the 16 | * execution of the given function by the defined time. 17 | */ 18 | const useTimeout = 19 | (fn: TCallback, milliseconds: number, options: UseTimeoutOptions = defaultOptions): [boolean, () => void] => { 20 | const opts = { ...defaultOptions, ...(options || {}) } 21 | const timeout = useRef() 22 | const callback = useRef(fn) 23 | const [isCleared, setIsCleared] = useState(false) 24 | 25 | // the clear method 26 | const clear = useCallback(() => { 27 | if (timeout.current) { 28 | clearTimeout(timeout.current) 29 | setIsCleared(true) 30 | } 31 | }, []) 32 | 33 | // if the provided function changes, change its reference 34 | useEffect(() => { 35 | if (isFunction(fn)) { 36 | callback.current = fn 37 | } 38 | }, [fn]) 39 | 40 | // when the milliseconds change, reset the timeout 41 | useEffect(() => { 42 | if (typeof milliseconds === 'number') { 43 | timeout.current = setTimeout(() => { 44 | callback.current() 45 | }, milliseconds) 46 | } 47 | return clear 48 | }, [milliseconds]) 49 | 50 | // when component unmount clear the timeout 51 | useEffect(() => () => { 52 | if (opts.cancelOnUnmount) { 53 | clear() 54 | } 55 | }, []) 56 | 57 | return [isCleared, clear] as [boolean, () => void] 58 | } 59 | 60 | export default useTimeout 61 | -------------------------------------------------------------------------------- /src/useToggle.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | /** 4 | * A quick and simple utility for toggle states 5 | */ 6 | const useToggle = (initialState = false): [boolean, () => void] => { 7 | const [value, setValue] = useState(initialState) 8 | 9 | const toggleState = useCallback(() => { 10 | setValue(!value) 11 | }, [value]) 12 | 13 | return [value, toggleState] 14 | } 15 | 16 | export default useToggle 17 | -------------------------------------------------------------------------------- /src/useTouch.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject } from 'react' 2 | import useTouchEvents, { type UseTouchEventsReturn } from './useTouchEvents' 3 | import useTouchState from './useTouchState' 4 | 5 | /** 6 | * Returns an array where the first item is the touch state from the `useTouchState` hook and the second item 7 | * is the object of callback setters from the `useTouchEvents` hook. 8 | * It is intended as a shortcut to those hooks. 9 | */ 10 | const useTouch = (targetRef: RefObject | undefined = undefined) => { 11 | const state = useTouchState(targetRef) 12 | const events = useTouchEvents(targetRef) 13 | 14 | return [state, events] as [TouchList, Readonly] 15 | } 16 | 17 | export default useTouch 18 | -------------------------------------------------------------------------------- /src/useTouchEvents.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject } from 'react' 2 | import useEvent from './useEvent' 3 | import { type CallbackSetter } from './shared/types' 4 | 5 | /** 6 | * Returns a frozen object of callback setters to handle the touch events. 7 | * It accepts a DOM ref representing the events target. 8 | * If a target is not provided the events will be globally attached to the document object. 9 | * 10 | * ### Shall the `useTouchEvents` callbacks replace the standard mouse handler props? 11 | * 12 | * **They shall not!** 13 | * **useTouchEvents is meant to be used to abstract more complex hooks that need to control mouse**, for instance: 14 | * a drag n drop hook. 15 | * Using useTouchEvents handlers instead of the classic props approach it's just as bad as it sounds since you'll 16 | * lose the React SyntheticEvent performance boost. 17 | * If you were doing something like the following: 18 | * 19 | */ 20 | const useTouchEvents = (targetRef?: RefObject, passive?: boolean) => { 21 | const target = targetRef ?? { current: window.document } as unknown as RefObject // hackish but works 22 | const onTouchStart = useEvent(target, 'touchstart', { passive }) 23 | const onTouchEnd = useEvent(target, 'touchend', { passive }) 24 | const onTouchCancel = useEvent(target, 'touchcancel', { passive }) 25 | const onTouchMove = useEvent(target, 'touchmove', { passive }) 26 | 27 | return Object.freeze({ 28 | onTouchStart, 29 | onTouchEnd, 30 | onTouchCancel, 31 | onTouchMove 32 | }) 33 | } 34 | 35 | /** 36 | * The return object of the `useTouchEvents` hook. 37 | */ 38 | export interface UseTouchEventsReturn { 39 | onTouchStart: CallbackSetter 40 | onTouchEnd: CallbackSetter 41 | onTouchCancel: CallbackSetter 42 | onTouchMove: CallbackSetter 43 | } 44 | 45 | export default useTouchEvents 46 | -------------------------------------------------------------------------------- /src/useTouchState.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject, useState } from 'react' 2 | import useTouchEvents from './useTouchEvents' 3 | 4 | /** 5 | * Returns the current touches from the touch move event. 6 | * It possibly accepts a DOM ref representing the mouse target. 7 | * If a target is not provided the state will be caught globally. 8 | */ 9 | const useTouchState = (targetRef?: RefObject) => { 10 | // eslint-disable-next-line @typescript-eslint/consistent-type-assertions 11 | const [state, setState] = useState({ length: 0 } as TouchList) 12 | const { onTouchStart, onTouchMove } = useTouchEvents(targetRef) 13 | 14 | onTouchStart((event: TouchEvent) => { 15 | setState(event.touches) 16 | }) 17 | 18 | onTouchMove((event: TouchEvent) => { 19 | setState(event.touches) 20 | }) 21 | 22 | return state 23 | } 24 | 25 | export default useTouchState 26 | -------------------------------------------------------------------------------- /src/useURLSearchParams.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useLocation } from 'react-router-dom' 3 | 4 | /** 5 | * Wraps the business logic of retrieve always updated URLSearchParams 6 | */ 7 | const useURLSearchParams = () => { 8 | const { search } = useLocation() 9 | 10 | return useMemo(() => new URLSearchParams(search), [search]) 11 | } 12 | 13 | export default useURLSearchParams 14 | -------------------------------------------------------------------------------- /src/useUnmount.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import isFunction from "./shared/isFunction"; 3 | import { type GenericFunction } from "./shared/types"; 4 | import createHandlerSetter from "./factory/createHandlerSetter"; 5 | 6 | /** 7 | * Returns a callback setter for a callback to be performed when the component did unmount. 8 | */ 9 | const useUnmount = ( 10 | callback?: TCallback 11 | ) => { 12 | const mountRef = useRef(false); 13 | const [handler, setHandler] = createHandlerSetter(callback); 14 | 15 | useEffect(() => { 16 | mountRef.current = true; 17 | 18 | return () => { 19 | if (isFunction(handler?.current) && mountRef.current) { 20 | handler.current(); 21 | } 22 | }; 23 | }, []); 24 | 25 | return setHandler; 26 | }; 27 | 28 | export default useUnmount; 29 | -------------------------------------------------------------------------------- /src/useUpdateEffect.ts: -------------------------------------------------------------------------------- 1 | import { type DependencyList, type EffectCallback, useEffect } from 'react' 2 | import useIsFirstRender from './useIsFirstRender' 3 | 4 | /** 5 | * A hook that runs an effect after the first render. 6 | * @param callback 7 | * @param deps 8 | */ 9 | const useUpdateEffect = (callback: EffectCallback, deps?: DependencyList) => { 10 | const isFirstRender = useIsFirstRender() 11 | 12 | useEffect(() => { 13 | if (!isFirstRender) { 14 | return callback() 15 | } 16 | 17 | return undefined 18 | }, deps) 19 | } 20 | 21 | export default useUpdateEffect 22 | -------------------------------------------------------------------------------- /src/useValidatedState.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef, useState } from 'react' 2 | 3 | /** 4 | * Returns a state that changes only if the next value pass its validator 5 | */ 6 | const useValidatedState = >(validator: TValidator, initialValue?: TValue) => { 7 | const [state, setState] = useState(initialValue) 8 | const validation = useRef({ changed: false }) 9 | 10 | const onChange = useCallback((nextValue: TValue) => { 11 | setState(nextValue) 12 | validation.current = { changed: true, valid: validator(nextValue) } 13 | }, [validator]) 14 | 15 | return [state, onChange, validation.current] as [TValue, (nextValue: TValue) => void, ValidationResult] 16 | } 17 | 18 | export type Validator = (value: TValue) => boolean 19 | 20 | export interface ValidationResult { 21 | changed: boolean 22 | valid?: boolean 23 | } 24 | 25 | export default useValidatedState 26 | -------------------------------------------------------------------------------- /src/useValueHistory.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | const distinctValues = (value: T, current: number, array: T[]): boolean => array.indexOf(value) === current 4 | 5 | /** 6 | * Accepts a variable (possibly a prop or a state) and returns its history (changes through updates). 7 | */ 8 | const useValueHistory = (value: TValue, distinct = false) => { 9 | const history = useRef([]) 10 | 11 | // quite simple 12 | useEffect(() => { 13 | history.current.push(value) 14 | 15 | if (distinct) { 16 | history.current = history.current.filter(distinctValues) 17 | } 18 | }, [value]) 19 | 20 | return history.current 21 | } 22 | 23 | export default useValueHistory 24 | -------------------------------------------------------------------------------- /src/useVerticalSwipe.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject } from 'react' 2 | import useSwipe, { type UseSwipeOptions } from './useSwipe' 3 | 4 | const defaultOptions: UseSwipeOptions = { 5 | threshold: 15, 6 | preventDefault: true 7 | } 8 | 9 | /** 10 | * A shortcut to useSwipe (with vertical options) 11 | * @param ref 12 | * @param options 13 | * @return {{alpha: number, count: number, swiping: boolean, direction: null}} 14 | */ 15 | const useVerticalSwipe = (ref?: RefObject, options: UseSwipeOptions = defaultOptions) => { 16 | const opts: UseSwipeOptions = { ...defaultOptions, ...(options || {}), ...{ direction: 'vertical' } } 17 | 18 | return useSwipe(ref, opts) 19 | } 20 | 21 | export default useVerticalSwipe 22 | -------------------------------------------------------------------------------- /src/useViewportSpy.ts: -------------------------------------------------------------------------------- 1 | import { type RefObject, useLayoutEffect, useState } from 'react' 2 | import isClient from './shared/isClient' 3 | import isApiSupported from './shared/isAPISupported' 4 | import isDevelopment from './shared/isDevelopment' 5 | import warnOnce from './shared/warnOnce' 6 | 7 | const defaultOptions: IntersectionObserverInit = { 8 | rootMargin: '0px', 9 | threshold: 0 10 | } 11 | 12 | const errorMessage = 'IntersectionObserver is not supported, this could happen both because' + 13 | ' window.IntersectionObserver is not supported by' + 14 | ' your current browser or you\'re using the useViewportSpy hook whilst server side rendering.' + 15 | ' This message is displayed only in development mode' 16 | 17 | /** 18 | * Uses the IntersectionObserverMock API to tell whether the given DOM Element (from useRef) is visible within the 19 | * viewport. 20 | */ 21 | const useViewportSpy = (ref: RefObject, options: IntersectionObserverInit = defaultOptions) => { 22 | if (!isClient || !isApiSupported('IntersectionObserver')) { 23 | if (isDevelopment) { 24 | warnOnce(errorMessage) 25 | } 26 | return false 27 | } 28 | 29 | const [isVisible, setIsVisible] = useState() 30 | 31 | useLayoutEffect(() => { 32 | const observer = new window.IntersectionObserver((entries) => { 33 | entries.forEach((item) => { 34 | const nextValue = item.isIntersecting 35 | setIsVisible(nextValue) 36 | }) 37 | }, options) 38 | 39 | if (ref.current) { 40 | observer.observe(ref.current) 41 | } 42 | 43 | return () => { 44 | observer.disconnect() 45 | } 46 | }, [ref]) 47 | 48 | return isVisible 49 | } 50 | 51 | export default useViewportSpy 52 | -------------------------------------------------------------------------------- /src/useViewportState.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import useWindowScroll from './useWindowScroll' 3 | import useWindowResize from './useWindowResize' 4 | import useThrottledCallback from './useThrottledCallback' 5 | import useDidMount from './useDidMount' 6 | 7 | export interface ViewportState { 8 | width: number 9 | height: number 10 | scrollX: number 11 | scrollY: number 12 | } 13 | 14 | /** 15 | * Returns updated information on the current viewport state 16 | */ 17 | const useViewportState = (debounceBy: number = 250) => { 18 | const [viewport, setViewport] = useState({ width: 0, height: 0, scrollY: 0, scrollX: 0 }) 19 | const onScroll = useWindowScroll() 20 | const onResize = useWindowResize() 21 | const onMount = useDidMount() 22 | 23 | const saveInfo = useThrottledCallback(() => { 24 | setViewport({ 25 | width: window.innerWidth, 26 | height: window.innerHeight, 27 | scrollX: window.scrollX, 28 | scrollY: window.scrollY 29 | }) 30 | }, [setViewport], debounceBy) 31 | 32 | onScroll(saveInfo) 33 | onResize(saveInfo) 34 | onMount(saveInfo) 35 | 36 | return viewport 37 | } 38 | 39 | export default useViewportState 40 | -------------------------------------------------------------------------------- /src/useWillUnmount.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useRef } from 'react' 2 | import isFunction from './shared/isFunction' 3 | import { type GenericFunction } from './shared/types' 4 | import createHandlerSetter from './factory/createHandlerSetter' 5 | 6 | /** 7 | * Returns a callback setter for a callback to be performed when the component will unmount. 8 | */ 9 | const useWillUnmount = (callback?: TCallback) => { 10 | const mountRef = useRef(false) 11 | const [handler, setHandler] = createHandlerSetter(callback) 12 | 13 | useLayoutEffect(() => { 14 | mountRef.current = true 15 | 16 | return () => { 17 | if (isFunction(handler?.current) && mountRef.current) { 18 | handler.current() 19 | } 20 | } 21 | }, []) 22 | 23 | return setHandler 24 | } 25 | 26 | export default useWillUnmount 27 | -------------------------------------------------------------------------------- /src/useWindowResize.ts: -------------------------------------------------------------------------------- 1 | import useGlobalEvent from './useGlobalEvent' 2 | 3 | /** 4 | * Returns a function that accepts a callback to be performed when the window resize. 5 | */ 6 | const useWindowResize = () => useGlobalEvent('resize') 7 | 8 | export default useWindowResize 9 | -------------------------------------------------------------------------------- /src/useWindowScroll.ts: -------------------------------------------------------------------------------- 1 | import useGlobalEvent from './useGlobalEvent' 2 | 3 | /** 4 | * Returns a function that accepts a callback to be performed when the window scrolls. 5 | */ 6 | const useWindowScroll = () => useGlobalEvent('scroll') 7 | 8 | export default useWindowScroll 9 | -------------------------------------------------------------------------------- /styleguide.config.js: -------------------------------------------------------------------------------- 1 | const { globSync } = require('glob') 2 | const path = require('path') 3 | const theme = require('./docs/utils/_styleguidist.theme.js') 4 | 5 | const srcPath = path.resolve(__dirname, 'src') 6 | const docsPath = path.resolve(__dirname, 'docs') 7 | 8 | const getHooksDocFiles = () => globSync(path.join(__dirname, 'docs', '[use]*.md')).map((filePath) => { 9 | const [filename] = filePath.match(/use[a-zA-Z]*/, 'gm') 10 | 11 | return ({ 12 | name: filename, content: `./docs/${filename}.md` 13 | }) 14 | }) 15 | 16 | module.exports = { 17 | title: 'beautiful-react-hooks - documentation', 18 | pagePerSection: true, 19 | exampleMode: 'expand', 20 | skipComponentsWithoutExample: true, 21 | styleguideDir: 'dist-ghpages', 22 | ribbon: { 23 | url: 'https://github.com/antonioru/beautiful-react-hooks', text: 'Fork me on GitHub' 24 | }, 25 | sections: [{ name: 'Introduction', content: './docs/Introduction.md', sectionDepth: 1 }, { 26 | name: 'Installation', 27 | content: './docs/Installation.md', 28 | sectionDepth: 1 29 | }, ...getHooksDocFiles()], 30 | require: [path.join(docsPath, 'utils', '_setup.js'), path.join(docsPath, 'utils', '_custom.css')], 31 | webpackConfig () { 32 | return { 33 | resolve: { 34 | alias: { 'beautiful-react-hooks': srcPath } 35 | }, 36 | module: { 37 | rules: [{ 38 | test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader' 39 | }, { 40 | test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ 41 | }, { 42 | test: /\.css$/i, use: ['style-loader', 'css-loader'] 43 | }, { 44 | test: /\.png$/, loader: 'url-loader' 45 | }] 46 | } 47 | } 48 | }, 49 | styleguideComponents: { 50 | LogoRenderer: path.join(docsPath, 'utils', '_CustomLogo'), 51 | PathlineRenderer: path.join(docsPath, 'utils', '_EmptyComponent'), 52 | ToolbarButtonRenderer: path.join(docsPath, 'utils', '_EmptyComponent') 53 | }, 54 | ...theme 55 | } 56 | -------------------------------------------------------------------------------- /test/_setup.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const sinon = require('sinon') 3 | const { createMemoryHistory } = require('history') 4 | 5 | global.history = createMemoryHistory() 6 | 7 | // shortcuts: 8 | global.expect = chai.expect 9 | global.should = chai.should() 10 | global.sinon = sinon 11 | 12 | // because of a bug in one of the project dependency `wait-for-expect`, the following line must be placed here. 13 | // to know more: https://github.com/testing-library/dom-testing-library/issues/194 14 | window.Date = Date 15 | -------------------------------------------------------------------------------- /test/geolocationUtils.spec.js: -------------------------------------------------------------------------------- 1 | import { geoStandardOptions, isSamePosition, makePositionObj } from '../dist/shared/geolocationUtils' 2 | import { positionMock } from './mocks/GeoLocationApi.mock' 3 | import assertFunction from './utils/assertFunction' 4 | 5 | describe('geolocation utils', () => { 6 | assertFunction(isSamePosition) 7 | assertFunction(makePositionObj) 8 | 9 | it('geoStandardOptions should be a frozen object defining standard geolocation options', () => { 10 | expect(geoStandardOptions).to.be.an('object').that.has.all.deep.keys('enableHighAccuracy', 'timeout', 'maximumAge') 11 | expect(geoStandardOptions).to.be.frozen 12 | }) 13 | 14 | it('isSamePosition should return false if nothing is provided', () => { 15 | const result = isSamePosition() 16 | 17 | expect(result).to.be.false 18 | }) 19 | 20 | it('isSamePosition should return false if invalid objects are provided', () => { 21 | expect(isSamePosition(null, {})).to.be.false 22 | expect(isSamePosition(null, null)).to.be.false 23 | expect(isSamePosition(positionMock, { current: false })).to.be.false 24 | }) 25 | 26 | it('isSamePosition should return false if the provided objects have different timestamp', () => { 27 | expect(isSamePosition(positionMock, { ...positionMock, timestamp: 200 })).to.be.false 28 | }) 29 | 30 | it('isSamePosition should return false if the provided objects are different', () => { 31 | const positionMock2 = { ...positionMock } 32 | positionMock2.coords = { ...positionMock.coords } 33 | positionMock2.coords.altitudeAccuracy = 60 34 | 35 | expect(isSamePosition(positionMock, positionMock2)).to.be.false 36 | }) 37 | 38 | it('isSamePosition should return true if the provided objects are equal', () => { 39 | const positionMock2 = { ...positionMock } 40 | 41 | expect(isSamePosition(positionMock, positionMock2)).to.be.true 42 | }) 43 | 44 | it('makePositionObj should return null if nothing is provided', () => { 45 | const result = makePositionObj() 46 | 47 | expect(result).to.be.null 48 | }) 49 | 50 | it('makePositionObj should remove unwanted property from a position object', () => { 51 | const pos = { 52 | ...positionMock, foo: 'bar', bar: 'foo' 53 | } 54 | 55 | const result = makePositionObj(pos) 56 | 57 | expect(result).to.be.deep.equal(positionMock) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /test/isAPISupported.spec.js: -------------------------------------------------------------------------------- 1 | import isAPISupported from '../dist/shared/isAPISupported' 2 | import assertFunction from './utils/assertFunction' 3 | 4 | describe('isAPISupported utility', () => { 5 | assertFunction(isAPISupported) 6 | 7 | it('should return true if an API is supported', () => { 8 | const result = isAPISupported('addEventListener') 9 | 10 | expect(result).to.be.true 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /test/isClient.spec.js: -------------------------------------------------------------------------------- 1 | import isClient from '../dist/shared/isClient' 2 | 3 | describe('isClient utility', () => { 4 | it('should be a boolean', () => expect(isClient).to.be.a('boolean')) 5 | it('should return true during tests', () => expect(isClient).to.be.true) 6 | }) 7 | -------------------------------------------------------------------------------- /test/mocks/AudioApi.mock.js: -------------------------------------------------------------------------------- 1 | class AudioApiMock extends window.Audio { 2 | src; 3 | state; 4 | duration; 5 | playing; 6 | constructor() { 7 | super(); 8 | 9 | this.src = ""; 10 | this.duration = NaN; 11 | this.playing = false; 12 | this.state = "STOPPED"; 13 | } 14 | play = () => { 15 | this.playing = true; 16 | this.state = "PLAYING"; 17 | 18 | return super.play(); 19 | }; 20 | pause = () => { 21 | this.playing = false; 22 | this.state = "PAUSED"; 23 | 24 | return super.pause(); 25 | }; 26 | } 27 | 28 | export default AudioApiMock; 29 | -------------------------------------------------------------------------------- /test/mocks/CookieStoreApi.mock.js: -------------------------------------------------------------------------------- 1 | const createCookieStoreApiMock = () => { 2 | const store = {}; 3 | 4 | const getItem = (key) => { 5 | return Promise.resolve({ name: key, value: store[key] }); 6 | } 7 | 8 | const deleteItem = (key) => { 9 | delete store[key]; 10 | 11 | return Promise.resolve(); 12 | } 13 | 14 | const setItem = ({ name, value }) => { 15 | store[name] = value; 16 | 17 | return Promise.resolve(); 18 | } 19 | 20 | return { 21 | get: getItem, 22 | set: setItem, 23 | delete: deleteItem, 24 | } 25 | } 26 | 27 | export default createCookieStoreApiMock(); 28 | 29 | -------------------------------------------------------------------------------- /test/mocks/GeoLocationApi.mock.js: -------------------------------------------------------------------------------- 1 | export const watchPositionSpy = sinon.spy() 2 | export const getCurrentPosition = sinon.spy() 3 | 4 | export const positionMock = { 5 | timestamp: 1, 6 | coords: { 7 | latitude: 1, 8 | longitude: 1, 9 | altitude: 1, 10 | accuracy: 1, 11 | altitudeAccuracy: 10, 12 | heading: 10, 13 | speed: 0 14 | } 15 | } 16 | 17 | const GeoLocationApiMock = { 18 | listeners: {}, 19 | getCurrentPosition(fn) { 20 | this.listeners.gcp = fn 21 | this.listeners.gcp(positionMock) 22 | getCurrentPosition(positionMock) 23 | }, 24 | watchPosition(success, error, options) { 25 | watchPositionSpy(options) 26 | 27 | this.listeners.s = success 28 | this.listeners.e = error 29 | }, 30 | clearWatch() { 31 | this.listeners = {} 32 | } 33 | } 34 | 35 | export default GeoLocationApiMock 36 | 37 | -------------------------------------------------------------------------------- /test/mocks/IntersectionObserver.mock.js: -------------------------------------------------------------------------------- 1 | class IntersectionObserverMock { 2 | constructor(fn) { 3 | this.connected = true 4 | this.fn = fn 5 | 6 | IntersectionObserverMock.instances.push(this) 7 | } 8 | 9 | observe() { 10 | if (this.connected) { 11 | this.fn([{ isIntersecting: true }]) 12 | } 13 | } 14 | 15 | disconnect() { 16 | this.connected = false 17 | } 18 | } 19 | 20 | IntersectionObserverMock.instances = [] 21 | IntersectionObserverMock.simulateObservation = () => { 22 | IntersectionObserverMock.instances.forEach((item) => item.observe()) 23 | } 24 | 25 | export default IntersectionObserverMock 26 | 27 | -------------------------------------------------------------------------------- /test/mocks/MatchMediaQueryList.mock.js: -------------------------------------------------------------------------------- 1 | const matchMediaQueryListMock = { 2 | listeners: {}, 3 | matches: true, 4 | addEventListener(cb) { 5 | this.listeners.cb = cb 6 | }, 7 | removeListener() { 8 | delete this.listeners.cb 9 | } 10 | } 11 | 12 | export default matchMediaQueryListMock; 13 | -------------------------------------------------------------------------------- /test/mocks/ResizeObserver.mock.js: -------------------------------------------------------------------------------- 1 | class ResizeObserverMock { 2 | constructor(fn) { 3 | this.fn = fn 4 | ResizeObserverMock.instances.push(this) 5 | } 6 | 7 | observe() { 8 | this.fn([{ 9 | contentRect: { bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0 } 10 | }]) 11 | } 12 | 13 | unobserve() { 14 | ResizeObserverMock.instances = [] 15 | } 16 | } 17 | 18 | ResizeObserverMock.instances = [] 19 | 20 | ResizeObserverMock.simulateResize = () => { 21 | ResizeObserverMock.instances.forEach((target) => { 22 | target.fn([{ 23 | contentRect: { bottom: 10, height: 10, left: 10, right: 10, top: 10, width: 10 } 24 | }]) 25 | }) 26 | } 27 | 28 | export default ResizeObserverMock 29 | -------------------------------------------------------------------------------- /test/mocks/SpeechSynthesis.mock.js: -------------------------------------------------------------------------------- 1 | export default { 2 | getVoices() { 3 | return [] 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /test/mocks/SpeechSynthesisUtterance.mock.js: -------------------------------------------------------------------------------- 1 | export default class SpeechSynthesisUtteranceMock { 2 | constructor(text) { 3 | this.text = text 4 | this.voice = {} 5 | this.pitch = 0 6 | this.rate = 0 7 | this.volume = 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/safeHasOwnProperty.spec.js: -------------------------------------------------------------------------------- 1 | import safeHasOwnProperty from '../dist/shared/safeHasOwnProperty' 2 | import assertFunction from './utils/assertFunction' 3 | 4 | describe('safeHasOwnProperty utility', () => { 5 | assertFunction(safeHasOwnProperty) 6 | 7 | it('should return false if nothing is provided', () => { 8 | const result = safeHasOwnProperty() 9 | 10 | expect(result).to.be.false 11 | }) 12 | 13 | it('should return true if the given object has the defined property', () => { 14 | const result = safeHasOwnProperty({ foo: 'bar' }, 'foo') 15 | 16 | expect(result).to.be.true 17 | }) 18 | 19 | it('should return false if the given object does not have the defined property', () => { 20 | const result = safeHasOwnProperty({ foo: 'bar' }, 'bar') 21 | 22 | expect(result).to.be.false 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /test/useDebouncedCallback.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup as cleanupHooks, renderHook } from '@testing-library/react-hooks' 3 | import { cleanup as cleanupReact, render } from '@testing-library/react' 4 | import useDebouncedCallback from '../dist/useDebouncedCallback' 5 | import promiseDelay from './utils/promiseDelay' 6 | import assertHook from './utils/assertHook' 7 | 8 | describe('useDebouncedCallback', () => { 9 | beforeEach(() => { 10 | cleanupReact() 11 | cleanupHooks() 12 | }) 13 | 14 | afterEach(sinon.restore) 15 | 16 | assertHook(useDebouncedCallback) 17 | 18 | it('should return a single function', () => { 19 | const fn = () => 0 20 | const { result } = renderHook(() => useDebouncedCallback(fn)) 21 | 22 | expect(result.current).to.be.a('function') 23 | }) 24 | 25 | it('should return a debounced function', async () => { 26 | const spy = sinon.spy() 27 | 28 | const TestComponent = () => { 29 | const debouncedCallback = useDebouncedCallback(() => { 30 | spy() 31 | }, [], 250) 32 | 33 | React.useEffect(() => { 34 | debouncedCallback() 35 | debouncedCallback() 36 | debouncedCallback() 37 | debouncedCallback() 38 | }, []) 39 | 40 | return 41 | } 42 | 43 | render() 44 | 45 | await promiseDelay(300) 46 | 47 | expect(spy.called).to.be.true 48 | expect(spy.callCount).to.equal(1) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/useDefaultedState.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useDefaultedState from '../dist/useDefaultedState' 3 | import assertHook from './utils/assertHook' 4 | 5 | describe('useDefaultedState', () => { 6 | beforeEach(() => cleanup()) 7 | 8 | assertHook(useDefaultedState) 9 | 10 | it('should return an array', () => { 11 | const { result } = renderHook(() => useDefaultedState(10)) 12 | 13 | expect(result.current).to.be.an('array') 14 | }) 15 | 16 | it('should default the state when null or undefined', () => { 17 | const defaultVal = 10 18 | const { result } = renderHook(() => useDefaultedState(defaultVal)) 19 | 20 | expect(result.current[0]).to.equal(defaultVal) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/useDidMount.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup as cleanupReact, render } from '@testing-library/react' 3 | import { cleanup as cleanupHooks, renderHook } from '@testing-library/react-hooks' 4 | import useDidMount from '../dist/useDidMount' 5 | import assertHook from './utils/assertHook' 6 | 7 | describe('useDidMount', () => { 8 | beforeEach(() => { 9 | cleanupHooks() 10 | cleanupReact() 11 | }) 12 | 13 | afterEach(sinon.restore) 14 | 15 | assertHook(useDidMount) 16 | 17 | it('should return a single function', () => { 18 | const { result } = renderHook(() => useDidMount()) 19 | 20 | expect(result.current).to.be.a('function') 21 | }) 22 | 23 | it('the returned function should be a setter for a callback to be performed when component did mount', () => { 24 | const spy = sinon.spy() 25 | 26 | const TestComponent = () => { 27 | const onMount = useDidMount() 28 | 29 | onMount(spy) 30 | 31 | return null 32 | } 33 | 34 | render() 35 | 36 | expect(spy.called).to.be.true 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/useDrag.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useDrag from '../dist/useDrag' 3 | import assertHook from './utils/assertHook' 4 | 5 | describe('useDrag', () => { 6 | beforeEach(() => cleanup()) 7 | 8 | assertHook(useDrag) 9 | 10 | it('should return an object the state of the current dragging element', () => { 11 | const targetRef = { current: document.createElement('div') } 12 | const { result } = renderHook(() => useDrag(targetRef)) 13 | 14 | expect(result.current).to.be.an('boolean') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /test/useDropZone.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useDropZone from '../dist/useDropZone' 3 | import assertHook from './utils/assertHook' 4 | 5 | describe('useDropZone', () => { 6 | beforeEach(() => cleanup()) 7 | 8 | assertHook(useDropZone) 9 | 10 | it('should return an object the state of the current dragging element', () => { 11 | const targetRef = { current: document.createElement('div') } 12 | const { result } = renderHook(() => useDropZone(targetRef)) 13 | 14 | expect(result.current).to.be.an('object').that.has.all.deep.keys('isOver', 'onDrop') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /test/useGeolocation.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useGeolocation from '../dist/useGeolocation' 3 | import GeoLocationApiMock, { watchPositionSpy } from './mocks/GeoLocationApi.mock' 4 | import assertHook from './utils/assertHook' 5 | 6 | describe('useGeolocation', () => { 7 | before(() => { 8 | window.navigator.geolocation = GeoLocationApiMock 9 | }) 10 | 11 | beforeEach(() => cleanup()) 12 | 13 | after(() => { 14 | delete window.navigator.geolocation 15 | }) 16 | 17 | assertHook(useGeolocation) 18 | 19 | it('should return an array where the first item is a geolocation state and the second an object of setters', () => { 20 | const { result } = renderHook(() => useGeolocation()) 21 | 22 | expect(result.current).to.be.an('array') 23 | expect(result.current.length).to.equal(2) 24 | expect(result.current[0]).to.be.a('object').that.has.all.deep.keys('isSupported', 'isRetrieving', 'onError', 'position') 25 | expect(result.current[1]).to.be.an('object').that.has.all.keys('isSupported', 'onChange', 'onError') 26 | }) 27 | 28 | it('the provided options should be passed down to the other hooks', () => { 29 | const optionsMock = { enableHighAccuracy: true } 30 | renderHook(() => useGeolocation(optionsMock)) 31 | 32 | GeoLocationApiMock.listeners.s() 33 | GeoLocationApiMock.listeners.e() 34 | 35 | expect(watchPositionSpy.called).to.be.true 36 | const lastOptions = watchPositionSpy.args[watchPositionSpy.callCount - 1][0] 37 | 38 | expect(lastOptions).to.equal(optionsMock) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /test/useGeolocationEvents.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup as cleanupReact, render } from '@testing-library/react' 3 | import { cleanup as cleanupHooks, renderHook } from '@testing-library/react-hooks' 4 | import useGeolocationEvents from '../dist/useGeolocationEvents' 5 | import GeoLocationApiMock, { watchPositionSpy } from './mocks/GeoLocationApi.mock' 6 | import assertHook from './utils/assertHook' 7 | 8 | describe('useGeolocationEvents', () => { 9 | before(() => { 10 | window.navigator.geolocation = GeoLocationApiMock 11 | }) 12 | 13 | beforeEach(() => { 14 | cleanupReact() 15 | cleanupHooks() 16 | sinon.reset() 17 | }) 18 | 19 | after(() => { 20 | delete window.navigator.geolocation 21 | }) 22 | 23 | assertHook(useGeolocationEvents) 24 | 25 | it('should return an object of geolocation-related callback setters', () => { 26 | const { result } = renderHook(() => useGeolocationEvents()) 27 | 28 | expect(result.current).to.be.an('object').that.has.all.deep.keys('isSupported', 'onChange', 'onError') 29 | expect(result.current).to.be.frozen 30 | }) 31 | 32 | it('should perform the onChange callback when geolocation changes', () => { 33 | const onChangeSpy = sinon.spy() 34 | const onErrorSpy = sinon.spy() 35 | 36 | const TestComponent = () => { 37 | const { onChange, onError } = useGeolocationEvents() 38 | 39 | onChange(onChangeSpy) 40 | onError(onErrorSpy) 41 | 42 | return 43 | } 44 | 45 | render() 46 | 47 | GeoLocationApiMock.listeners.s() 48 | GeoLocationApiMock.listeners.e() 49 | 50 | expect(onChangeSpy.called).to.be.true 51 | }) 52 | 53 | it('should accept an options object to be used as a parameter when calling watchPosition', () => { 54 | const optionsMock = { foo: 'bar' } 55 | 56 | const TestComponent = () => { 57 | const { isSupported } = useGeolocationEvents(optionsMock) 58 | 59 | return {isSupported} 60 | } 61 | 62 | render() 63 | 64 | GeoLocationApiMock.listeners.s() 65 | GeoLocationApiMock.listeners.e() 66 | 67 | expect(watchPositionSpy.called).to.be.true 68 | const lastOptions = watchPositionSpy.args[watchPositionSpy.callCount - 1][0] 69 | 70 | expect(lastOptions).to.equal(optionsMock) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /test/useGlobalEvent.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup as cleanupReact, fireEvent, render } from '@testing-library/react' 3 | import { cleanup as cleanupHooks, renderHook } from '@testing-library/react-hooks' 4 | import { renderHook as renderServerHook } from '@testing-library/react-hooks/server' 5 | import useGlobalEvent from '../dist/useGlobalEvent' 6 | import assertHook from './utils/assertHook' 7 | import noop from '../dist/shared/noop' 8 | 9 | describe('useGlobalEvent', () => { 10 | beforeEach(() => { 11 | cleanupReact() 12 | cleanupHooks() 13 | }) 14 | 15 | assertHook(useGlobalEvent) 16 | 17 | it('should return a single function', () => { 18 | const { result } = renderHook(() => useGlobalEvent('resize')) 19 | 20 | expect(result.current).to.be.a('function') 21 | }) 22 | 23 | it('the returned function should be a callback setter that fires when the event occurs', () => { 24 | const spy = sinon.spy() 25 | 26 | const TestComponent = () => { 27 | const onWindowResize = useGlobalEvent('resize') 28 | 29 | onWindowResize(spy) 30 | 31 | return null 32 | } 33 | 34 | render() 35 | 36 | const resizeEvent = window.document.createEvent('UIEvents') 37 | resizeEvent.initUIEvent('resize', true, false, window, 0) 38 | 39 | fireEvent(window, resizeEvent) 40 | 41 | expect(spy.called).to.be.true 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /test/useHandlerSetter.spec.js: -------------------------------------------------------------------------------- 1 | import { act, cleanup, renderHook } from '@testing-library/react-hooks' 2 | import createHandlerSetter from '../dist/factory/createHandlerSetter' 3 | import assertFunction from './utils/assertFunction' 4 | 5 | describe('createHandlerSetter', () => { 6 | beforeEach(() => cleanup()) 7 | 8 | assertFunction(createHandlerSetter) 9 | 10 | it('should return an array of 2 elements', () => { 11 | const { result } = renderHook(() => createHandlerSetter()) 12 | 13 | expect(result.current).to.be.an.instanceOf(Array) 14 | expect(result.current.length).to.equal(2) 15 | }) 16 | 17 | it('should return the reference to a handler', () => { 18 | const { result } = renderHook(() => createHandlerSetter()) 19 | const [handlerRef] = result.current 20 | 21 | expect(handlerRef.current).to.be.undefined 22 | expect(handlerRef).to.be.an('object').that.has.all.keys('current') 23 | }) 24 | 25 | it('should return a handler setter', () => { 26 | const { result } = renderHook(() => createHandlerSetter()) 27 | const [handlerRef, setHandlerRef] = result.current 28 | 29 | const fooCallback = () => undefined 30 | 31 | expect(setHandlerRef).to.be.a('function') 32 | 33 | act(() => { 34 | setHandlerRef(fooCallback) 35 | }) 36 | 37 | expect(handlerRef.current).to.equal(fooCallback) 38 | }) 39 | 40 | it('the setter should throw when changing the handler to an invalid value', () => { 41 | const { result } = renderHook(() => createHandlerSetter()) 42 | const [, setHandlerRef] = result.current 43 | 44 | const shouldThrow = () => { 45 | setHandlerRef({ foo: 'bar' }) 46 | } 47 | 48 | expect(shouldThrow).to.throw() 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/useInfiniteScroll.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useInfiniteScroll from '../dist/useInfiniteScroll' 3 | import assertHook from './utils/assertHook' 4 | 5 | describe('useInfiniteScroll', () => { 6 | beforeEach(() => cleanup()) 7 | 8 | assertHook(useInfiniteScroll) 9 | 10 | it('should return an callback setter', () => { 11 | const ref = { current: document.createElement('div') } 12 | const { result } = renderHook(() => useInfiniteScroll(ref)) 13 | 14 | expect(result.current).to.be.a('function') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /test/useInterval.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { act, cleanup, renderHook } from '@testing-library/react-hooks' 3 | import useInterval from '../dist/useInterval' 4 | import assertHook from './utils/assertHook' 5 | 6 | describe('useInterval', () => { 7 | beforeEach(() => cleanup()) 8 | afterEach(sinon.restore) 9 | 10 | assertHook(useInterval) 11 | 12 | it('should return an array, the first item is the interval state whilst the second its clearing method', () => { 13 | const { result } = renderHook(() => useInterval(() => null, 1000)) 14 | 15 | expect(result.current).to.be.an('array') 16 | expect(result.current[0]).to.be.an('boolean') 17 | expect(result.current[1]).to.be.a('function') 18 | }) 19 | 20 | it('even if the provided options is null, it should keep working', () => { 21 | const { result } = renderHook(() => useInterval(() => null, 1000, null)) 22 | 23 | expect(result.current).to.be.an('array') 24 | }) 25 | 26 | it('should allow to clear the created interval', () => { 27 | const spy = sinon.spy() 28 | const ms = 100 29 | const { result, error } = renderHook(() => useInterval(spy, ms)) 30 | const clear = result.current[1] 31 | 32 | expect(result.current[0]).to.be.false 33 | 34 | act(clear) 35 | 36 | expect(result.current[0]).to.be.true 37 | expect(spy.called).to.be.false 38 | 39 | act(clear) 40 | 41 | expect(result.current[0]).to.be.true 42 | 43 | expect(error).to.be.undefined 44 | }) 45 | 46 | it('should check the received parameters to avoid errors', () => { 47 | const { result } = renderHook(() => useInterval(10, { foo: 'bar' })) 48 | const clear = result.current[1] 49 | 50 | expect(result.current[0]).to.be.false 51 | expect(clear).to.be.a('function') 52 | 53 | act(clear) 54 | 55 | expect(result.current[0]).to.be.false 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /test/useIsFirstRender.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, render } from '@testing-library/react' 3 | 4 | import assertHook from './utils/assertHook' 5 | import useIsFirstRender from '../dist/useIsFirstRender' 6 | 7 | describe('useIsFirstRender', () => { 8 | beforeEach(() => { 9 | cleanup() 10 | }) 11 | 12 | assertHook(useIsFirstRender) 13 | 14 | it('should return isFirstRender flag set to true before the first render and then always false', () => { 15 | const TestComponent = ({ isAfterRerender }) => { 16 | const isFirstRender = useIsFirstRender(); 17 | 18 | if (!isAfterRerender) { 19 | expect(isFirstRender).to.be.eq(true) 20 | } else { 21 | expect(isFirstRender).to.be.eq(false) 22 | } 23 | 24 | return 25 | } 26 | 27 | const { rerender } = render() 28 | 29 | rerender() 30 | rerender() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /test/useLifecycle.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useLifecycle from '../dist/useLifecycle' 3 | import assertHook from './utils/assertHook' 4 | 5 | describe('useLifecycle', () => { 6 | beforeEach(() => cleanup()) 7 | 8 | assertHook(useLifecycle) 9 | 10 | it('the returned function should wrap other lifecycle hooks', () => { 11 | const { result } = renderHook(() => useLifecycle()) 12 | 13 | expect(result.current).to.be.an('object').that.has.all.keys('onDidMount', 'onWillUnmount') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/useLongPress.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useLongPress from '../dist/useLongPress' 3 | import assertHook from './utils/assertHook' 4 | 5 | describe('useLongPress', () => { 6 | beforeEach(() => cleanup()) 7 | 8 | assertHook(useLongPress) 9 | 10 | it('should return a boolean value reporting whether the long-press event is happening as well as the handlers setters', () => { 11 | const ref = { current: document.createElement('div') } 12 | const { result } = renderHook(() => useLongPress(ref)) 13 | 14 | expect(result.current.isLongPressing).to.be.a('boolean') 15 | expect(result.current.onLongPressStart).to.be.a('function') 16 | expect(result.current.onLongPressEnd).to.be.a('function') 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/useMediaQuery.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | 3 | import assertHook from './utils/assertHook' 4 | import useMediaQuery from '../dist/useMediaQuery' 5 | import mediaQueryListMock from './mocks/MatchMediaQueryList.mock' 6 | 7 | describe('useMediaQuery', () => { 8 | beforeEach(() => { 9 | cleanup() 10 | }) 11 | 12 | afterEach(() => { 13 | sinon.restore() 14 | }) 15 | 16 | assertHook(useMediaQuery) 17 | 18 | it('should return a boolean value', () => { 19 | window.matchMedia = () => (mediaQueryListMock) 20 | const { result } = renderHook(() => useMediaQuery('(min-width: 1024px)')) 21 | 22 | expect(result.current).to.be.a('boolean') 23 | 24 | delete window.matchMedia 25 | }) 26 | 27 | it('should warn when the window.matchMedia API is not supported', () => { 28 | delete window.matchMedia 29 | const warnSpy = sinon.spy(console, 'warn') 30 | const { result } = renderHook(() => useMediaQuery('(min-width: 1024px)')) 31 | 32 | expect(result.current).to.be.false 33 | expect(warnSpy.called).to.be.true 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /test/useMouse.spec.js: -------------------------------------------------------------------------------- 1 | import { act, cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useMouse from '../dist/useMouse' 3 | import assertHook from './utils/assertHook' 4 | 5 | describe('useMouse', () => { 6 | beforeEach(() => cleanup()) 7 | 8 | assertHook(useMouse) 9 | 10 | it('should return an array where the first item is a mouse state and the second a group of setters', () => { 11 | const ref = { current: document.createElement('div') } 12 | const { result } = renderHook(() => useMouse(ref)) 13 | 14 | expect(result.current).to.be.an('array') 15 | expect(result.current.length).to.equal(2) 16 | expect(result.current[0]).to.be.a('object').that.has.all.keys('clientX', 'clientY', 'screenY', 'screenY') 17 | expect(result.current[1]).to.be.an('object').that.has.all.keys( 18 | 'onMouseDown', 'onMouseEnter', 'onMouseLeave', 'onMouseMove', 'onMouseOut', 'onMouseOver', 'onMouseUp' 19 | ) 20 | }) 21 | 22 | it('should work without a ref provided ', () => { 23 | const positionMock = { clientX: 10, clientY: 10, screenX: 30, screenY: 30 } 24 | const { result } = renderHook(() => useMouse()) 25 | 26 | act(() => { 27 | const mouseEvent = new MouseEvent('mousemove', positionMock) 28 | document.dispatchEvent(mouseEvent) 29 | }) 30 | 31 | expect(result.current[0]).to.deep.equal(positionMock) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/useMouseState.spec.js: -------------------------------------------------------------------------------- 1 | import { act, cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useMouseState from '../dist/useMouseState' 3 | import assertHook from './utils/assertHook' 4 | 5 | describe('useMouseState', () => { 6 | beforeEach(() => cleanup()) 7 | 8 | assertHook(useMouseState) 9 | 10 | it('should return a mouse coordinates reporting object', () => { 11 | const { result } = renderHook(() => useMouseState()) 12 | 13 | expect(result.current).to.be.a('object').that.has.all.keys('clientX', 'clientY', 'screenY', 'screenY') 14 | }) 15 | 16 | it('should update the mouse position whilst it moves', () => { 17 | const refMock = { current: document.createElement('div') } 18 | const positionMock = { clientX: 10, clientY: 10, screenX: 30, screenY: 30 } 19 | const { result } = renderHook(() => useMouseState(refMock)) 20 | 21 | act(() => { 22 | const mouseEvent = new MouseEvent('mousemove', positionMock) 23 | refMock.current.dispatchEvent(mouseEvent) 24 | }) 25 | 26 | expect(result.current).to.deep.equal(positionMock) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/useMutableState.spec.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { render } from '@testing-library/react' 3 | import { cleanup, renderHook } from '@testing-library/react-hooks' 4 | import useMutableState from '../dist/useMutableState' 5 | import assertHook from './utils/assertHook' 6 | 7 | describe('useMutableState', () => { 8 | beforeEach(cleanup) 9 | 10 | assertHook(useMutableState) 11 | 12 | it('should return an object', () => { 13 | const { result } = renderHook(() => useMutableState({ value: 0 })) 14 | 15 | expect(result.current).to.be.an('object').that.has.property('value') 16 | }) 17 | 18 | it('should re-render when the value changes', () => { 19 | const spy = sinon.spy() 20 | 21 | const TestComponent = () => { 22 | const state = useMutableState({ value: 0 }) 23 | 24 | spy() 25 | 26 | useEffect(() => { 27 | state.value = 1 28 | }, []) 29 | 30 | return val: {state.value} 31 | } 32 | 33 | render() 34 | 35 | expect(spy.callCount).to.equal(2) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/useObjectState.spec.js: -------------------------------------------------------------------------------- 1 | import { act, cleanup, renderHook } from "@testing-library/react-hooks"; 2 | 3 | import assertHook from "./utils/assertHook"; 4 | import useObjectState from "../dist/useObjectState"; 5 | 6 | describe("useObjectState", () => { 7 | beforeEach(() => cleanup()); 8 | 9 | assertHook(useObjectState); 10 | 11 | it("should return updated object state", async () => { 12 | const { result, waitFor } = renderHook(() => 13 | useObjectState({ test: "test", test1: "test1" }) 14 | ); 15 | 16 | const [state, setState] = result.current; 17 | 18 | expect(state) 19 | .to.be.an("object") 20 | .that.has.deep.equal({ test: "test", test1: "test1" }); 21 | 22 | act(() => { 23 | setState({ test1: "it works" }); 24 | }); 25 | 26 | await waitFor(() => { 27 | expect(result.current[0]) 28 | .to.be.an("object") 29 | .that.has.deep.equal({ test: "test", test1: "it works" }); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/useObservable.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { from } from 'rxjs' 3 | import { cleanup, renderHook } from '@testing-library/react-hooks' 4 | import useObservable from '../dist/useObservable' 5 | import assertHook from './utils/assertHook' 6 | 7 | describe('useObservable', () => { 8 | beforeEach(() => cleanup()) 9 | 10 | afterEach(sinon.restore) 11 | 12 | assertHook(useObservable) 13 | 14 | it('should return a function', (done) => { 15 | const observer = renderHook(() => useObservable(from([1]), () => done())) 16 | expect(observer).to.be.a('function') 17 | }) 18 | 19 | it('should subscribe correctly', (done) => { 20 | const numbers$ = from([1, 2, 3, 4, 5]) 21 | const expected = [] 22 | renderHook(() => useObservable(numbers$, (result) => expected.push(result))) 23 | expect(expected).to.have.lengthOf(5) 24 | done() 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /test/useOnlineState.spec.js: -------------------------------------------------------------------------------- 1 | import { fireEvent } from '@testing-library/react' 2 | import { cleanup, renderHook } from '@testing-library/react-hooks' 3 | import useOnlineState from '../dist/useOnlineState' 4 | import assertHook from './utils/assertHook' 5 | 6 | describe('useOnlineState', () => { 7 | beforeEach(() => { 8 | cleanup() 9 | window.ononline = null 10 | }) 11 | 12 | assertHook(useOnlineState) 13 | 14 | it('should return a boolean value', () => { 15 | const { result } = renderHook(() => useOnlineState('resize')) 16 | 17 | expect(result.current).to.be.a('boolean') 18 | }) 19 | 20 | it('should return true if the device not support online event', () => { 21 | delete window.ononline 22 | 23 | const spy = sinon.spy(window.console, 'warn') 24 | const { result } = renderHook(() => useOnlineState()) 25 | expect(spy.calledOnce).to.be.true 26 | expect(result.current).to.be.true 27 | }) 28 | 29 | it('should change after an online/offline event', () => { 30 | const { result } = renderHook(() => useOnlineState()) 31 | expect(result.current).to.be.true 32 | 33 | const offlineEvent = window.document.createEvent('Event') 34 | offlineEvent.initEvent('offline', false, false) 35 | fireEvent(window, offlineEvent) 36 | expect(result.current).to.be.false 37 | 38 | const onlineEvent = window.document.createEvent('Event') 39 | onlineEvent.initEvent('online', false, false) 40 | fireEvent(window, onlineEvent) 41 | expect(result.current).to.be.true 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /test/usePreviousValue.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup as cleanupReact, render } from '@testing-library/react' 3 | import { cleanup as cleanupHooks, renderHook } from '@testing-library/react-hooks' 4 | import usePreviousValue from '../dist/usePreviousValue' 5 | import assertHook from './utils/assertHook' 6 | 7 | describe('usePreviousValue', () => { 8 | beforeEach(() => { 9 | cleanupHooks() 10 | cleanupReact() 11 | }) 12 | 13 | afterEach(sinon.restore) 14 | 15 | assertHook(usePreviousValue) 16 | 17 | it('should return undefined after the first render', () => { 18 | const { result } = renderHook(() => usePreviousValue(10)) 19 | 20 | expect(result.current).to.be.undefined 21 | }) 22 | 23 | it('should return the previous value of a given variable', () => { 24 | const TestComponent = (props) => { 25 | // eslint-disable-next-line react/prop-types 26 | const { value } = props 27 | const prev = usePreviousValue(value) 28 | 29 | return {prev} 30 | } 31 | 32 | const { container, rerender } = render() 33 | rerender() 34 | 35 | expect(container.querySelector('p').innerHTML).to.equal('1') 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/useQueryParam.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useQueryParam from '../dist/useQueryParam' 3 | import assertHook from './utils/assertHook' 4 | import ReactRouterWrapper from './utils/ReactRouterWrapper' 5 | 6 | describe('useQueryParam', () => { 7 | beforeEach(() => cleanup()) 8 | 9 | assertHook(useQueryParam) 10 | 11 | it('should work similar to useState', () => { 12 | const initialValue = 'bar' 13 | const { result } = renderHook(() => useQueryParam('foo', { initialValue }), { wrapper: ReactRouterWrapper }) 14 | const [val, setVal] = result.current 15 | 16 | expect(val).to.be.a('string') 17 | expect(val).to.equal(initialValue) 18 | expect(setVal).to.be.a('function') 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/useQueryParams.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useQueryParams from '../dist/useQueryParams' 3 | import assertHook from './utils/assertHook' 4 | import ReactRouterWrapper from './utils/ReactRouterWrapper' 5 | 6 | describe('useQueryParams', () => { 7 | beforeEach(() => cleanup()) 8 | 9 | assertHook(useQueryParams) 10 | 11 | it('should work similar to useState', () => { 12 | const initialValue = ['1', '2', '3'] 13 | const { result } = renderHook(() => useQueryParams('foo[]', { initialValue }), { wrapper: ReactRouterWrapper }) 14 | const [val, setVal] = result.current 15 | 16 | expect(val).to.be.an('array') 17 | expect(val).to.deep.equal(initialValue) 18 | expect(setVal).to.be.a('function') 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/useRenderInfo.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, renderHook } from '@testing-library/react-hooks' 3 | import useRenderInfo from '../dist/useRenderInfo' 4 | import assertHook from './utils/assertHook' 5 | 6 | describe('useRenderInfo', () => { 7 | beforeEach(() => cleanup()) 8 | 9 | afterEach(sinon.restore) 10 | 11 | assertHook(useRenderInfo) 12 | 13 | it('should return an information object', () => { 14 | const name = 'Foo' 15 | const { result: { current: info } } = renderHook(() => useRenderInfo(name, false)) 16 | 17 | expect(info).to.be.an('object') 18 | expect(info.module).to.equal(name) 19 | expect(info.renders).to.be.a('number') 20 | expect(info.sinceLast).to.be.a('string') 21 | expect(info.timestamp).to.be.a('number') 22 | }) 23 | 24 | it('should print consistent information', () => { 25 | const { result: { current: info }, rerender } = renderHook(() => useRenderInfo('foo', false)) 26 | 27 | rerender() 28 | rerender() 29 | 30 | expect(info.renders).to.equal(3) 31 | }) 32 | 33 | it('should print renders information in group', () => { 34 | const groupSpy = sinon.spy(console, 'group') 35 | const groupEndSpy = sinon.spy(console, 'groupEnd') 36 | const logSpy = sinon.spy(console, 'log') 37 | 38 | renderHook(() => useRenderInfo(name, true)) 39 | 40 | expect(logSpy.called).to.be.true 41 | expect(groupSpy.called).to.be.true 42 | expect(groupEndSpy.called).to.be.true 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /test/useRequestAnimationFrame.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup as cleanupReact, render } from '@testing-library/react' 3 | import { cleanup as cleanupHooks, renderHook } from '@testing-library/react-hooks' 4 | import useRequestAnimationFrame from '../dist/useRequestAnimationFrame' 5 | import promiseDelay from './utils/promiseDelay' 6 | import assertHook from './utils/assertHook' 7 | 8 | describe('useRequestAnimationFrame', () => { 9 | beforeEach(() => { 10 | cleanupReact() 11 | cleanupHooks() 12 | if (window.requestAnimationFrame) { 13 | window.requestAnimationFrame = (fn) => fn() 14 | } 15 | }) 16 | 17 | afterEach(sinon.restore) 18 | 19 | assertHook(useRequestAnimationFrame) 20 | 21 | it('should immediately perform the given function', () => { 22 | window.requestAnimationFrame = (fn) => fn() 23 | const spy = sinon.spy() 24 | 25 | renderHook(() => useRequestAnimationFrame(spy)) 26 | 27 | expect(spy.called).to.be.true 28 | expect(spy.args[0][0]).to.be.a('number') 29 | expect(spy.args[0][1]).to.be.a('function') 30 | 31 | delete window.requestAnimationFrame 32 | }) 33 | 34 | it('should return an onFinish callback to be performed when the animation finishes', async () => { 35 | window.requestAnimationFrame = (fn) => setTimeout(fn, 1) 36 | 37 | const spy = sinon.spy() 38 | 39 | const TestComponent = () => { 40 | const onFinish = useRequestAnimationFrame((c, next) => next(), { increment: 5, finishAt: 50, startAt: 0 }) 41 | 42 | onFinish(spy) 43 | 44 | return 45 | } 46 | 47 | render() 48 | 49 | await promiseDelay(500) 50 | 51 | expect(spy.called).to.be.true 52 | delete window.requestAnimationFrame 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /test/useResizeObserver.spec.js: -------------------------------------------------------------------------------- 1 | import { act, cleanup, renderHook } from '@testing-library/react-hooks' 2 | import { expect } from 'chai' 3 | import useResizeObserver from '../dist/useResizeObserver' 4 | import ResizeObserverMock from './mocks/ResizeObserver.mock' 5 | import promiseDelay from './utils/promiseDelay' 6 | import assertHook from './utils/assertHook' 7 | 8 | describe('useResizeObserver', () => { 9 | const originalRO = global.ResizeObserver 10 | 11 | before(() => { 12 | global.ResizeObserver = window.ResizeObserver = ResizeObserverMock 13 | }) 14 | 15 | beforeEach(() => { 16 | cleanup() 17 | }) 18 | 19 | after(() => { 20 | global.ResizeObserver = window.ResizeObserver = originalRO 21 | }) 22 | 23 | assertHook(useResizeObserver) 24 | 25 | it('should return undefined when first initialised', () => { 26 | const refMock = { current: document.createElement('div') } 27 | const { result } = renderHook(() => useResizeObserver(refMock, 100)) 28 | expect(result.current).to.be.undefined 29 | }) 30 | 31 | it('should return a single function', async () => { 32 | const refMock = { current: document.createElement('div') } 33 | const { result, rerender } = renderHook(() => useResizeObserver(refMock, 0)) 34 | 35 | act(() => { 36 | ResizeObserver.simulateResize(refMock.current) 37 | }) 38 | 39 | rerender() 40 | 41 | await promiseDelay(250) // wait 250ms to let the debounced fn to perform 42 | 43 | return expect(result.current).to.be.an('object') 44 | }) 45 | }) 46 | 47 | describe('useResizeObserver (when the API is not supported)', () => { 48 | const originalRO = global.ResizeObserver 49 | 50 | beforeEach(() => { 51 | delete global.ResizeObserver 52 | delete window.ResizeObserver 53 | }) 54 | 55 | afterEach(() => { 56 | global.ResizeObserver = window.ResizeObserver = originalRO 57 | sinon.restore() 58 | }) 59 | 60 | it('should not observe anything', async () => { 61 | const refMock = { current: document.createElement('div') } 62 | const warnSpy = sinon.spy(console, 'warn') 63 | 64 | const { result } = renderHook(() => useResizeObserver(refMock)) 65 | 66 | expect(warnSpy.called).to.be.true 67 | expect(result.current).to.be.undefined 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /test/useSearchQuery.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useSearchQuery from '../dist/useSearchQuery' 3 | import assertHook from './utils/assertHook' 4 | import ReactRouterWrapper from './utils/ReactRouterWrapper' 5 | 6 | describe('useSearchQuery', () => { 7 | beforeEach(() => cleanup()) 8 | 9 | assertHook(useSearchQuery) 10 | 11 | it('should work similar to useState', () => { 12 | const initialValue = 'foo' 13 | const { result } = renderHook(() => useSearchQuery(initialValue), { wrapper: ReactRouterWrapper }) 14 | const [val, setVal] = result.current 15 | 16 | expect(val).to.be.a('string') 17 | expect(val).to.equal(initialValue) 18 | expect(setVal).to.be.a('function') 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/useSpeechRecognition.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useSpeechRecognition from '../dist/useSpeechRecognition' 3 | import SpeechSynthesisUtteranceMock from './mocks/SpeechSynthesisUtterance.mock' 4 | import SpeechSynthesisMock from './mocks/SpeechSynthesis.mock' 5 | import assertHook from './utils/assertHook' 6 | 7 | describe('useSpeechRecognition', () => { 8 | const originalSpeechSynth = global.speechSynthesis 9 | const originalSpeechSynthesisUtterance = global.SpeechSynthesisUtterance 10 | 11 | before(() => { 12 | global.speechSynthesis = SpeechSynthesisMock 13 | global.SpeechSynthesisUtterance = SpeechSynthesisUtteranceMock 14 | }) 15 | 16 | beforeEach(() => cleanup()) 17 | 18 | after(() => { 19 | global.SpeechSynthesisUtterance = originalSpeechSynthesisUtterance 20 | global.speechSynthesis = originalSpeechSynth 21 | }) 22 | 23 | assertHook(useSpeechRecognition) 24 | 25 | it('should return an object containing the speak function and the utter', () => { 26 | const { result } = renderHook(() => useSpeechRecognition()) 27 | 28 | expect(result.current).to.be.an('object') 29 | expect(result.current.startRecording).to.be.a('function') 30 | expect(result.current.stopRecording).to.be.a('function') 31 | expect(result.current.transcript).to.be.a('string') 32 | expect(result.current.isRecording).to.be.a('boolean') 33 | expect(result.current.isSupported).to.be.a('boolean') 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /test/useSpeechSynthesis.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useSpeechSynthesis from '../dist/useSpeechSynthesis' 3 | import SpeechSynthesisUtteranceMock from './mocks/SpeechSynthesisUtterance.mock' 4 | import SpeechSynthesisMock from './mocks/SpeechSynthesis.mock' 5 | import assertHook from './utils/assertHook' 6 | 7 | describe('useSpeechSynthesis', () => { 8 | const originalSpeechSynth = global.speechSynthesis 9 | const originalSpeechSynthesisUtterance = global.SpeechSynthesisUtterance 10 | 11 | before(() => { 12 | global.speechSynthesis = SpeechSynthesisMock 13 | global.SpeechSynthesisUtterance = SpeechSynthesisUtteranceMock 14 | }) 15 | 16 | beforeEach(() => cleanup()) 17 | 18 | after(() => { 19 | global.SpeechSynthesisUtterance = originalSpeechSynthesisUtterance 20 | global.speechSynthesis = originalSpeechSynth 21 | }) 22 | 23 | assertHook(useSpeechSynthesis) 24 | 25 | it('should return an object containing the speak function and the utter', () => { 26 | const { result } = renderHook(() => useSpeechSynthesis('text', { volume: 1, pitch: 1, rate: 1 })) 27 | 28 | expect(result.current).to.be.an('object') 29 | expect(result.current.speak).to.be.a('function') 30 | expect(result.current.speechSynthUtterance).to.be.an('object') 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /test/useStorage.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup as cleanupHooks } from '@testing-library/react-hooks' 3 | import createStorageHook from '../dist/factory/createStorageHook' 4 | import assertFunction from './utils/assertFunction' 5 | 6 | describe('createStorageHook', () => { 7 | beforeEach(cleanupHooks) 8 | 9 | afterEach(sinon.restore) 10 | 11 | assertFunction(createStorageHook) 12 | 13 | it('should return a function', () => { 14 | const useLocalStorage = createStorageHook('local') 15 | expect(useLocalStorage).to.be.a('function') 16 | }) 17 | 18 | it('should warn when an invalid storage name is provided', () => { 19 | const warnSpy = sinon.spy(console, 'warn') 20 | 21 | createStorageHook('foo') 22 | 23 | expect(warnSpy.called).to.be.true 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/useSwipe.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useSwipe from '../dist/useSwipe' 3 | import useHorizontalSwipe from '../dist/useHorizontalSwipe' 4 | import useVerticalSwipe from '../dist/useVerticalSwipe' 5 | import assertHook from './utils/assertHook' 6 | 7 | describe('useSwipe', () => { 8 | beforeEach(() => cleanup()) 9 | 10 | assertHook(useSwipe) 11 | 12 | it('should return the swipe state', () => { 13 | const { result } = renderHook(() => useSwipe()) 14 | 15 | expect(result.current).to.be.an('object').that.has.all.keys('swiping', 'direction', 'alphaX', 'alphaY', 'count') 16 | }) 17 | }) 18 | 19 | describe('useHorizontalSwipe', () => { 20 | beforeEach(() => cleanup()) 21 | 22 | it('should be a function', () => { 23 | expect(useHorizontalSwipe).to.be.a('function') 24 | }) 25 | 26 | it('should return the swipe state', () => { 27 | const { result } = renderHook(() => useHorizontalSwipe()) 28 | 29 | expect(result.current).to.be.an('object').that.has.all.keys('swiping', 'direction', 'alphaX', 'alphaY', 'count') 30 | }) 31 | }) 32 | 33 | describe('useVerticalSwipe', () => { 34 | beforeEach(() => cleanup()) 35 | 36 | it('should be a function', () => { 37 | expect(useVerticalSwipe).to.be.a('function') 38 | }) 39 | 40 | it('should return the swipe state', () => { 41 | const { result } = renderHook(() => useVerticalSwipe()) 42 | 43 | expect(result.current).to.be.an('object').that.has.all.keys('swiping', 'direction', 'alphaX', 'alphaY', 'count') 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /test/useSwipeEvents.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useSwipeEvents from '../dist/useSwipeEvents' 3 | import assertHook from './utils/assertHook' 4 | 5 | describe('useSwipeEvents', () => { 6 | beforeEach(() => cleanup()) 7 | 8 | assertHook(useSwipeEvents) 9 | 10 | it('should return the swipe handler setters', () => { 11 | const { result } = renderHook(() => useSwipeEvents()) 12 | 13 | expect(result.current).to.be.an('object').that.has.all.keys( 14 | 'onSwipeLeft', 'onSwipeRight', 'onSwipeStart', 'onSwipeMove', 'onSwipeEnd', 'onSwipeUp', 'onSwipeDown' 15 | ) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/useSystemVoices.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useSystemVoices from '../dist/useSystemVoices' 3 | import SpeechSynthesisMock from './mocks/SpeechSynthesis.mock' 4 | import assertHook from './utils/assertHook' 5 | 6 | describe('useSystemVoices', () => { 7 | const originalSpeechSynth = window.speechSynthesis 8 | 9 | before(() => { 10 | window.speechSynthesis = SpeechSynthesisMock 11 | }) 12 | 13 | beforeEach(() => cleanup()) 14 | 15 | after(() => { 16 | window.speechSynthesis = originalSpeechSynth 17 | }) 18 | 19 | assertHook(useSystemVoices) 20 | 21 | it('should return the list of all available system voices', () => { 22 | const { result } = renderHook(() => useSystemVoices()) 23 | 24 | expect(result.current).to.be.an('array') 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /test/useThrottledCallback.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup as cleanupHooks, renderHook } from '@testing-library/react-hooks' 3 | import { cleanup as cleanupReact, render } from '@testing-library/react' 4 | import useThrottledCallback from '../dist/useThrottledCallback' 5 | import promiseDelay from './utils/promiseDelay' 6 | import assertHook from './utils/assertHook' 7 | 8 | describe('useThrottledCallback', () => { 9 | beforeEach(() => { 10 | cleanupReact() 11 | cleanupHooks() 12 | }) 13 | 14 | afterEach(sinon.restore) 15 | 16 | assertHook(useThrottledCallback) 17 | 18 | it('should return a single function', () => { 19 | const fn = () => 0 20 | const { result } = renderHook(() => useThrottledCallback(fn)) 21 | 22 | expect(result.current).to.be.a('function') 23 | }) 24 | 25 | it('should return a throttled function', async () => { 26 | const spy = sinon.spy() 27 | 28 | const TestComponent = () => { 29 | const throttledFn = useThrottledCallback(() => { 30 | spy() 31 | }, [], 250) 32 | 33 | React.useEffect(() => { 34 | throttledFn() 35 | throttledFn() 36 | throttledFn() 37 | throttledFn() 38 | }, []) 39 | 40 | return 41 | } 42 | 43 | render() 44 | 45 | await promiseDelay(300) 46 | 47 | expect(spy.called).to.be.true 48 | expect(spy.callCount).to.equal(1) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/useToggle.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { renderHook } from '@testing-library/react-hooks' 3 | import { fireEvent, render } from '@testing-library/react' 4 | import useToggle from '../dist/useToggle' 5 | import assertHook from './utils/assertHook' 6 | 7 | describe('useToggle', () => { 8 | 9 | assertHook(useToggle) 10 | 11 | it('first item should be a boolean', () => { 12 | const { result } = renderHook(() => useToggle()) 13 | 14 | expect(result.current).to.be.an('array') 15 | expect(result.current[0]).to.be.a('boolean') 16 | }) 17 | 18 | it('second item should be a React setState function', () => { 19 | const { result } = renderHook(() => useToggle()) 20 | 21 | expect(result.current).to.be.an('array') 22 | expect(result.current[1]).to.be.a('function') 23 | }) 24 | 25 | it('should toggle boolean values ', () => { 26 | const TestComponent = () => { 27 | const [toggle, changeToggle] = useToggle(true) 28 | 29 | return {toggle ? 'on' : 'off'} 30 | } 31 | 32 | const { container } = render() 33 | const button = container.querySelector('button') 34 | 35 | expect(button.innerHTML).to.equal('on') 36 | 37 | fireEvent.click(button) 38 | 39 | expect(button.innerHTML).to.equal('off') 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /test/useTouchEvents.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useTouchEvents from '../dist/useTouchEvents' 3 | import assertHook from './utils/assertHook' 4 | 5 | describe('useTouchEvents', () => { 6 | beforeEach(() => cleanup()) 7 | 8 | assertHook(useTouchEvents) 9 | 10 | it('should return an object of mouse-related callback setters', () => { 11 | const { result } = renderHook(() => useTouchEvents()) 12 | 13 | expect(result.current).to.be.an('object').that.has.all.keys('onTouchStart', 'onTouchEnd', 'onTouchMove', 'onTouchCancel') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/useTouchState.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useTouchState from '../dist/useTouchState' 3 | import assertHook from './utils/assertHook' 4 | 5 | describe('useTouchState', () => { 6 | beforeEach(() => cleanup()) 7 | 8 | assertHook(useTouchState) 9 | 10 | it('should return a TouchList object', () => { 11 | const { result } = renderHook(() => useTouchState()) 12 | 13 | expect(result.current).to.have.property('length') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/useURLSearchParams.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useURLSearchParams from '../dist/useURLSearchParams' 3 | import assertHook from './utils/assertHook' 4 | import ReactRouterWrapper from './utils/ReactRouterWrapper' 5 | 6 | describe('useURLSearchParams', () => { 7 | beforeEach(() => cleanup()) 8 | 9 | assertHook(useURLSearchParams) 10 | 11 | it('should return an instance of URLSearchParams', () => { 12 | const { result } = renderHook(() => useURLSearchParams(), { wrapper: ReactRouterWrapper }) 13 | expect(result.current).to.be.an.instanceOf(URLSearchParams) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/useUnmount.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup as cleanupReact, render } from '@testing-library/react' 3 | import { cleanup as cleanupHooks, renderHook } from '@testing-library/react-hooks' 4 | import useUnmount from '../dist/useUnmount' 5 | import assertHook from './utils/assertHook' 6 | 7 | describe('useUnmount', () => { 8 | beforeEach(() => { 9 | cleanupHooks() 10 | cleanupReact() 11 | }) 12 | 13 | assertHook(useUnmount) 14 | 15 | it('should return a single function', () => { 16 | const { result } = renderHook(() => useUnmount()) 17 | 18 | expect(result.current).to.be.a('function') 19 | }) 20 | 21 | it('the returned function should be a setter for a callback to be performed when component did unmount', () => { 22 | const spy = sinon.spy() 23 | 24 | const TestComponent = () => { 25 | const onUnmount = useUnmount() 26 | 27 | onUnmount(spy) 28 | 29 | return null 30 | } 31 | 32 | const { rerender } = render() 33 | 34 | rerender(null) 35 | 36 | expect(spy.called).to.be.true 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/useUpdateEffect.spec.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { cleanup, renderHook } from '@testing-library/react-hooks' 3 | 4 | import assertHook from './utils/assertHook' 5 | import useUpdateEffect from '../dist/useUpdateEffect' 6 | 7 | describe('useUpdateEffect', () => { 8 | beforeEach(() => { 9 | cleanup() 10 | }) 11 | 12 | assertHook(useUpdateEffect) 13 | 14 | it('should represent directly the difference between useEffect and useUpdateEffect', () => { 15 | const useEffectCallbackSpy = sinon.spy() 16 | const useUpdateEffectCallbackSpy = sinon.spy() 17 | 18 | const { rerender: rerenderUseEffect } = renderHook(() => useEffect(useEffectCallbackSpy)) 19 | const { rerender: rerenderUseUpdateEffect } = renderHook(() => useUpdateEffect(useUpdateEffectCallbackSpy)) 20 | 21 | expect(useEffectCallbackSpy.called).to.be.true 22 | expect(useUpdateEffectCallbackSpy.called).to.be.false 23 | 24 | rerenderUseEffect(); 25 | rerenderUseUpdateEffect() 26 | 27 | expect(useEffectCallbackSpy.called).to.be.true 28 | expect(useUpdateEffectCallbackSpy.called).to.be.true 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/useValidatedState.spec.js: -------------------------------------------------------------------------------- 1 | import { act, cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useValidatedState from '../dist/useValidatedState' 3 | import assertHook from './utils/assertHook' 4 | 5 | describe('useValidatedState', () => { 6 | const makeValidator = (value = true) => () => value 7 | 8 | beforeEach(() => cleanup()) 9 | 10 | assertHook(useValidatedState) 11 | 12 | it('should return an array of state, setState and validation', () => { 13 | const initialState = 10 14 | const { result } = renderHook(() => useValidatedState(makeValidator(), initialState)) 15 | 16 | expect(result.current).to.be.an('array') 17 | expect(result.current[0]).to.be.equal(initialState) 18 | expect(result.current[1]).to.a('function') 19 | expect(result.current[2]).to.an('object') 20 | }) 21 | 22 | it('should return the validated state', () => { 23 | const initialState = 10 24 | const { result } = renderHook(() => useValidatedState(makeValidator(true), initialState)) 25 | const [, setState] = result.current 26 | 27 | act(() => { 28 | setState(20) 29 | }) 30 | 31 | expect(result.current[0]).to.equal(20) 32 | expect(result.current[2]).to.deep.equal({ changed: true, valid: true }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/useValueHistory.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useValueHistory from '../dist/useValueHistory' 3 | import assertHook from './utils/assertHook' 4 | 5 | describe('useValueHistory', () => { 6 | beforeEach(() => cleanup()) 7 | 8 | assertHook(useValueHistory) 9 | 10 | it('should return an array', () => { 11 | const { result } = renderHook(() => useValueHistory(10)) 12 | 13 | expect(result.current).to.be.an('array') 14 | }) 15 | 16 | it('should return the history of the given value', () => { 17 | const { result, rerender } = renderHook((value) => useValueHistory(value), { initialProps: 1 }) 18 | 19 | rerender(2) 20 | rerender(3) 21 | 22 | expect(result.current).to.deep.equal([1, 2, 3]) 23 | }) 24 | 25 | it('should return the history of the unique given value', async () => { 26 | const { result, rerender } = renderHook((value) => useValueHistory(value, true), { initialProps: 1 }) 27 | 28 | rerender(2) 29 | rerender(1) 30 | rerender(1) 31 | 32 | expect(result.current).to.deep.equal([1, 2]) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/useViewportSpy.spec.js: -------------------------------------------------------------------------------- 1 | import { act, cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useViewportSpy from '../dist/useViewportSpy' 3 | import IntersectionObserverMock from './mocks/IntersectionObserver.mock' 4 | import assertHook from './utils/assertHook' 5 | 6 | describe('useViewportSpy', () => { 7 | before(() => { 8 | window.IntersectionObserver = IntersectionObserverMock 9 | }) 10 | 11 | beforeEach(() => cleanup()) 12 | 13 | after(() => { 14 | delete window.IntersectionObserver 15 | }) 16 | 17 | assertHook(useViewportSpy) 18 | 19 | it('should return a single boolean value', () => { 20 | const refMock = { current: document.createElement('div') } 21 | const { result } = renderHook(() => useViewportSpy(refMock)) 22 | 23 | expect(result.current).to.be.a('boolean') 24 | }) 25 | 26 | it('should spy on the viewport', () => { 27 | const refMock = { current: document.createElement('div') } 28 | const { result } = renderHook(() => useViewportSpy(refMock, { threshold: 0.2 })) 29 | 30 | act(() => { 31 | IntersectionObserverMock.simulateObservation() 32 | }) 33 | 34 | expect(result.current).to.be.true 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /test/useViewportState.spec.js: -------------------------------------------------------------------------------- 1 | import { cleanup, renderHook } from '@testing-library/react-hooks' 2 | import useViewportState from '../dist/useViewportState' 3 | import assertHook from './utils/assertHook' 4 | 5 | describe('useViewportState', () => { 6 | 7 | beforeEach(() => cleanup()) 8 | 9 | assertHook(useViewportState) 10 | 11 | it('should return an object containing information on the current window state', () => { 12 | const { result } = renderHook(() => useViewportState()) 13 | 14 | expect(result.current).to.be.an('object').that.has.all.keys('width', 'height', 'scrollY', 'scrollX') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /test/useWillUnmount.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup as cleanupReact, render } from '@testing-library/react' 3 | import { cleanup as cleanupHooks, renderHook } from '@testing-library/react-hooks' 4 | import useWillUnmount from '../dist/useWillUnmount' 5 | import assertHook from './utils/assertHook' 6 | 7 | describe('useWillUnmount', () => { 8 | beforeEach(() => { 9 | cleanupHooks() 10 | cleanupReact() 11 | }) 12 | 13 | assertHook(useWillUnmount) 14 | 15 | it('should return a single function', () => { 16 | const { result } = renderHook(() => useWillUnmount()) 17 | 18 | expect(result.current).to.be.a('function') 19 | }) 20 | 21 | it('the returned function should be a setter for a callback to be performed when component will unmount', () => { 22 | const spy = sinon.spy() 23 | 24 | const TestComponent = () => { 25 | const onUnmount = useWillUnmount() 26 | 27 | onUnmount(spy) 28 | 29 | return null 30 | } 31 | 32 | const { rerender } = render() 33 | 34 | rerender(null) 35 | 36 | expect(spy.called).to.be.true 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/useWindowResize.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup as cleanupReact, fireEvent, render } from '@testing-library/react' 3 | import { cleanup as cleanupHooks, renderHook } from '@testing-library/react-hooks' 4 | import useWindowResize from '../dist/useWindowResize' 5 | import assertHook from './utils/assertHook' 6 | 7 | describe('useWindowResize', () => { 8 | beforeEach(() => { 9 | cleanupReact() 10 | cleanupHooks() 11 | }) 12 | 13 | assertHook(useWindowResize) 14 | 15 | it('should return a single function', () => { 16 | const { result } = renderHook(() => useWindowResize()) 17 | 18 | expect(result.current).to.be.a('function') 19 | }) 20 | 21 | it('the returned function should be a setter for a callback to be performed when window resizes', () => { 22 | const spy = sinon.spy() 23 | 24 | const TestComponent = () => { 25 | const onWindowResize = useWindowResize() 26 | 27 | onWindowResize(spy) 28 | 29 | return null 30 | } 31 | 32 | render() 33 | 34 | const resizeEvent = window.document.createEvent('UIEvents') 35 | resizeEvent.initUIEvent('resize', true, false, window, 0) 36 | 37 | fireEvent(window, resizeEvent) 38 | 39 | expect(spy.called).to.be.true 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /test/useWindowScroll.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup as cleanupReact, fireEvent, render } from '@testing-library/react' 3 | import { cleanup as cleanupHooks, renderHook } from '@testing-library/react-hooks' 4 | import useWindowScroll from '../dist/useWindowScroll' 5 | import assertHook from './utils/assertHook' 6 | 7 | describe('useWindowScroll', () => { 8 | beforeEach(() => { 9 | cleanupReact() 10 | cleanupHooks() 11 | }) 12 | 13 | assertHook(useWindowScroll) 14 | 15 | it('should return a single function', () => { 16 | const { result } = renderHook(() => useWindowScroll()) 17 | 18 | expect(result.current).to.be.a('function') 19 | }) 20 | 21 | it('the returned function should be a setter for a callback to be performed when window scrolls', () => { 22 | const spy = sinon.spy() 23 | 24 | const TestComponent = () => { 25 | const onWindowScroll = useWindowScroll() 26 | 27 | onWindowScroll(spy) 28 | 29 | return null 30 | } 31 | 32 | render() 33 | 34 | const resizeEvent = window.document.createEvent('UIEvents') 35 | resizeEvent.initUIEvent('scroll', true, false, window, 0) 36 | 37 | fireEvent(window, resizeEvent) 38 | 39 | expect(spy.called).to.be.true 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /test/utils/ReactRouterWrapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { MemoryRouter } from 'react-router-dom' 3 | 4 | const ReactRouterWrapper = ({ children }) => ( 5 | 6 | {children} 7 | ) 8 | 9 | export default ReactRouterWrapper 10 | -------------------------------------------------------------------------------- /test/utils/assertFunction.js: -------------------------------------------------------------------------------- 1 | const assertFunction = (fn) => { 2 | it(`${fn.name || 'it'} should be a function`, () => { 3 | expect(fn).to.be.a('function') 4 | }) 5 | } 6 | 7 | export default assertFunction 8 | -------------------------------------------------------------------------------- /test/utils/assertHook.js: -------------------------------------------------------------------------------- 1 | import assertFunction from './assertFunction' 2 | 3 | const assertHook = (hook) => { 4 | assertFunction(hook) 5 | 6 | it('name should start with \'use\'', () => { 7 | expect(hook.name.substring(0, 3)).to.equal('use') 8 | }) 9 | } 10 | 11 | export default assertHook 12 | -------------------------------------------------------------------------------- /test/utils/promiseDelay.js: -------------------------------------------------------------------------------- 1 | const promiseDelay = (delay = 1000) => new Promise((resolve) => { 2 | setTimeout(() => { 3 | resolve() 4 | }, delay) 5 | }) 6 | 7 | export default promiseDelay 8 | -------------------------------------------------------------------------------- /test/warnOnce.spec.js: -------------------------------------------------------------------------------- 1 | import { createSandbox } from 'sinon' 2 | import assertFunction from './utils/assertFunction' 3 | import warnOnce from '../dist/shared/warnOnce' 4 | 5 | describe('warnOnce', () => { 6 | assertFunction(warnOnce) 7 | 8 | const sandbox = createSandbox() 9 | 10 | beforeEach(() => { 11 | sandbox.spy(console, 'warn') 12 | }) 13 | 14 | afterEach(() => { 15 | sandbox.restore() 16 | }) 17 | 18 | it('should not warn the same message twice', () => { 19 | const message = 'foo' 20 | const repeats = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 21 | 22 | repeats.forEach(() => warnOnce(message)) 23 | 24 | expect(console.warn.calledOnce).to.be.true 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "target": "es6", 6 | "outDir": "./dist/esm" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*.ts" 4 | ], 5 | "compilerOptions": { 6 | "allowSyntheticDefaultImports": true, 7 | "noImplicitAny": true, 8 | "target": "es5", 9 | "jsx": "react", 10 | "allowJs": false, 11 | "moduleResolution": "node", 12 | "esModuleInterop": true, 13 | "strictNullChecks": true, 14 | "rootDir": "./src", 15 | "outDir": "./dist" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": true, 5 | "declaration": true, 6 | "target": "es2020" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /usage_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioru/beautiful-react-hooks/21399d635bd75679aac7d53821dd3e54c93d635c/usage_example.png --------------------------------------------------------------------------------
{JSON.stringify(state, null, '\t')}
Current value of 'foo' param is '{params.get('foo')}'
Change the value of the foo param to see how this hook works
{prev}