├── .babelrc ├── .gitattributes ├── .github └── workflows │ ├── deploy-docs.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src ├── globals.d.ts ├── useAnimationFrame.ts ├── useBreakpoint.ts ├── useCallbackRef.ts ├── useCommittedRef.ts ├── useCustomEffect.ts ├── useDebouncedCallback.ts ├── useDebouncedState.ts ├── useDebouncedValue.ts ├── useEventCallback.ts ├── useEventListener.ts ├── useFocusManager.ts ├── useForceUpdate.ts ├── useGlobalListener.ts ├── useImage.ts ├── useImmediateUpdateEffect.ts ├── useIntersectionObserver.ts ├── useInterval.ts ├── useIsInitialRenderRef.ts ├── useIsomorphicEffect.ts ├── useMap.ts ├── useMediaQuery.ts ├── useMergeState.ts ├── useMergeStateFromProps.ts ├── useMergedRefs.ts ├── useMountEffect.ts ├── useMounted.ts ├── useMutationObserver.ts ├── usePrevious.ts ├── useRafInterval.ts ├── useRefWithInitialValueFactory.ts ├── useResizeObserver.ts ├── useSafeState.ts ├── useSet.ts ├── useStableMemo.ts ├── useStateAsync.ts ├── useThrottledEventHandler.ts ├── useTimeout.ts ├── useToggleState.ts ├── useUpdateEffect.ts ├── useUpdateImmediateEffect.ts ├── useUpdateLayoutEffect.ts ├── useUpdatedRef.ts └── useWillUnmount.ts ├── test ├── helpers.tsx ├── setup.js ├── tsconfig.json ├── useAnimationFrame.test.tsx ├── useBreakpoint.ssr.test.tsx ├── useBreakpoint.test.tsx ├── useCallbackRef.test.tsx ├── useCommittedRef.test.tsx ├── useCustomEffect.test.tsx ├── useDebouncedCallback.test.tsx ├── useDebouncedState.test.tsx ├── useDebouncedValue.test.tsx ├── useForceUpdate.test.tsx ├── useImmediateUpdateEffect.test.tsx ├── useIntersectionObserver.test.tsx ├── useInterval.test.tsx ├── useIsInitialRenderRef.test.tsx ├── useIsomorphicEffect.ssr.test.tsx ├── useIsomorphicEffect.test.tsx ├── useMap.test.tsx ├── useMediaQuery.ssr.test.tsx ├── useMediaQuery.test.tsx ├── useMergeStateFromProps.test.tsx ├── useMergedRefs.test.tsx ├── useMountEffect.test.tsx ├── useMounted.test.tsx ├── useMutationObserver.test.tsx ├── usePrevious.test.tsx ├── useRefWithInitialValueFactory.test.tsx ├── useSafeState.test.tsx ├── useSet.test.tsx ├── useStateAsync.test.tsx ├── useThrottledEventHandler.test.tsx ├── useTimeout.test.tsx ├── useToggleState.test.tsx ├── useUpdateEffect.test.tsx ├── useUpdateLayoutEffect.test.tsx └── useWillUnmount.test.tsx ├── tsconfig.json ├── vitest.config.mts ├── www ├── .babelrc ├── .gitignore ├── gatsby-config.js ├── gatsby-node.js ├── package.json ├── src │ ├── @docpocalypse │ │ └── gatsby-theme │ │ │ └── components │ │ │ └── .gitkeep │ ├── examples │ │ └── .gitkeep │ └── pages │ │ └── index.mdx ├── tailwind.config.js ├── templates │ └── component.js └── yarn.lock └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@4c", "@babel/typescript"], 3 | "env": { 4 | "esm": { 5 | "presets": [ 6 | [ 7 | "@4c", 8 | { 9 | "modules": false 10 | } 11 | ] 12 | 13 | ] 14 | }, 15 | "test": { 16 | "presets": [["@4c", { "development": true }], "@babel/typescript"] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Documentation 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | build_docs: 8 | name: Build and Deploy Documentation 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checking out Repository 12 | uses: actions/checkout@v1 13 | - name: Setup Node.js 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 12.x 17 | - name: Install Dependencies 18 | run: yarn bootstrap 19 | - name: Build and Deploy Production Files 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | run: yarn deploy-docs 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | test: 11 | name: Run Testing Suite 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checking out Repository 15 | uses: actions/checkout@v2 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 18.x 20 | - name: Install Dependencies 21 | run: yarn bootstrap 22 | - name: Run Tests 23 | run: yarn test 24 | - name: Build Project 25 | run: yarn build 26 | - name: Upload Test Coverage to CodeCov 27 | uses: codecov/codecov-action@v1.0.2 28 | with: 29 | token: ${{ secrets.CODECOV_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | cjs/ 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.6.2](https://github.com/react-restart/hooks/compare/v0.6.1...v0.6.2) (2025-01-07) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * exports definition again ([364e4a6](https://github.com/react-restart/hooks/commit/364e4a68ccad6e30d59c1ec6fd305019a521f912)) 7 | 8 | 9 | 10 | 11 | 12 | ## [0.6.1](https://github.com/react-restart/hooks/compare/v0.6.0...v0.6.1) (2025-01-07) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * exports definition ([523780d](https://github.com/react-restart/hooks/commit/523780d5bd7ce9647753c284d84f4f59e624af70)) 18 | 19 | 20 | 21 | 22 | 23 | # [0.6.0](https://github.com/react-restart/hooks/compare/v0.5.1...v0.6.0) (2025-01-07) 24 | 25 | 26 | * build!: Update build, export real ESM (#100) ([9e272d3](https://github.com/react-restart/hooks/commit/9e272d3c5e89c7cd6547c6c66afac6b800b6c7ff)), closes [#100](https://github.com/react-restart/hooks/issues/100) 27 | 28 | 29 | ### BREAKING CHANGES 30 | 31 | * exports native esm modules and removes the index file in favor of sub imports 32 | 33 | 34 | 35 | 36 | 37 | ## [0.5.1](https://github.com/jquense/react-common-hooks/compare/v0.5.0...v0.5.1) (2025-01-02) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * **useTimeout:** isPending never updates ([51e14ec](https://github.com/jquense/react-common-hooks/commit/51e14ec0d40c70b4cf09ce02b58beaa740b6b0cf)) 43 | 44 | 45 | 46 | 47 | 48 | # [0.5.0](https://github.com/jquense/react-common-hooks/compare/v0.4.16...v0.5.0) (2024-11-25) 49 | 50 | 51 | * A number of StrictMode fixes and updates (#99) ([1511129](https://github.com/jquense/react-common-hooks/commit/1511129898f9b95659bab7f964fe33528d04b938)), closes [#99](https://github.com/jquense/react-common-hooks/issues/99) 52 | 53 | 54 | ### BREAKING CHANGES 55 | 56 | * no longer supports `cancelPrevious` this is always true 57 | 58 | * fix:(useDebouncedCallback): Clean up timeout logic in strict mode 59 | 60 | * chore: deprecate useWillUnmount 61 | 62 | This hook is not possible in StrictMode, and can cause bugs 63 | 64 | * fix(useForceUpdate): ensure that chained calls produce an update 65 | 66 | * Update useCustomEffect.ts 67 | 68 | * address feedback 69 | 70 | 71 | 72 | 73 | 74 | ## [0.4.16](https://github.com/jquense/react-common-hooks/compare/v0.4.15...v0.4.16) (2024-02-10) 75 | 76 | 77 | ### Bug Fixes 78 | 79 | * **useDebouncedCallback:** fix behavior in strict mode ([#98](https://github.com/jquense/react-common-hooks/issues/98)) ([9a01792](https://github.com/jquense/react-common-hooks/commit/9a01792e025d35bc6af92582af5f8dd928227f2d)) 80 | 81 | 82 | 83 | 84 | 85 | ## [0.4.15](https://github.com/jquense/react-common-hooks/compare/v0.4.14...v0.4.15) (2023-12-07) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * **useDebouncedState:** Add factory function overload to type, clarify setter purity ([9aa18fb](https://github.com/jquense/react-common-hooks/commit/9aa18fb48d06ab2bd4d64b1b862a80d5d4d5cdd5)) 91 | 92 | 93 | 94 | 95 | 96 | ## [0.4.14](https://github.com/jquense/react-common-hooks/compare/v0.4.13...v0.4.14) (2023-12-07) 97 | 98 | 99 | ### Bug Fixes 100 | 101 | * **useDebouncedCallback:** wrap handler in useEventCallback so it's not stale ([63f897a](https://github.com/jquense/react-common-hooks/commit/63f897aa6972f66fd5d076c5edb4da6cda5031c0)) 102 | 103 | 104 | 105 | 106 | 107 | ## [0.4.13](https://github.com/jquense/react-common-hooks/compare/v0.4.12...v0.4.13) (2023-12-06) 108 | 109 | 110 | ### Features 111 | 112 | * **useDebouncedCallback:** return value from debounced function ([d1ead47](https://github.com/jquense/react-common-hooks/commit/d1ead4728fb14949a00d937f72a0657e0552d5b9)) 113 | 114 | 115 | 116 | 117 | 118 | ## [0.4.12](https://github.com/jquense/react-common-hooks/compare/v0.4.11...v0.4.12) (2023-12-06) 119 | 120 | 121 | ### Features 122 | 123 | * add leading, and maxWait options to debounce hooks ([#97](https://github.com/jquense/react-common-hooks/issues/97)) ([c8d69b2](https://github.com/jquense/react-common-hooks/commit/c8d69b23dd22a802f1c918f791478e841a9d0b99)) 124 | 125 | 126 | 127 | 128 | 129 | ## [0.4.11](https://github.com/jquense/react-common-hooks/compare/v0.4.10...v0.4.11) (2023-07-17) 130 | 131 | 132 | ### Bug Fixes 133 | 134 | * **useIsInitialRenderRef:** clarify usage and fix ordering ([437c417](https://github.com/jquense/react-common-hooks/commit/437c417806e501c17e89b7252168e790077bb322)) 135 | 136 | 137 | 138 | 139 | 140 | ## [0.4.10](https://github.com/jquense/react-common-hooks/compare/v0.4.9...v0.4.10) (2023-07-13) 141 | 142 | 143 | ### Features 144 | 145 | * **useFocusManager:** fire focused change immediately ([#92](https://github.com/jquense/react-common-hooks/issues/92)) ([7bb6d5a](https://github.com/jquense/react-common-hooks/commit/7bb6d5ae74c40ff80b2fa5db06e21b7e970f7455)) 146 | 147 | 148 | 149 | 150 | 151 | ## [0.4.9](https://github.com/jquense/react-common-hooks/compare/v0.4.8...v0.4.9) (2023-02-10) 152 | 153 | 154 | ### Features 155 | 156 | * add exports field in package.json ([#88](https://github.com/jquense/react-common-hooks/issues/88)) ([1c325b0](https://github.com/jquense/react-common-hooks/commit/1c325b08a693ae9efa8cbfac6cc8d5884d2d238f)) 157 | 158 | 159 | 160 | 161 | 162 | ## [0.4.8](https://github.com/jquense/react-common-hooks/compare/v0.4.7...v0.4.8) (2023-01-25) 163 | 164 | 165 | ### Bug Fixes 166 | 167 | * **useDebouncedCallback:** fix return type ([#85](https://github.com/jquense/react-common-hooks/issues/85)) ([f2e26a0](https://github.com/jquense/react-common-hooks/commit/f2e26a0cbf8753f3c5964452b05b50721e5b3ca3)) 168 | 169 | 170 | 171 | 172 | 173 | ## [0.4.7](https://github.com/jquense/react-common-hooks/compare/v0.4.6...v0.4.7) (2022-04-27) 174 | 175 | 176 | ### Features 177 | 178 | * **useBreakpoint:** add xxl breakpoint ([#75](https://github.com/jquense/react-common-hooks/issues/75)) ([8021cea](https://github.com/jquense/react-common-hooks/commit/8021ceacaf34304e460193faf1ff8c5e31f43db1)) 179 | 180 | 181 | 182 | 183 | 184 | ## [0.4.6](https://github.com/jquense/react-common-hooks/compare/v0.4.5...v0.4.6) (2022-04-01) 185 | 186 | 187 | ### Bug Fixes 188 | 189 | * **useMounted:** adjust to work with strict effects ([#70](https://github.com/jquense/react-common-hooks/issues/70)) ([8deba46](https://github.com/jquense/react-common-hooks/commit/8deba46157fd8d73194cbd6ad81e5a6d575bf5d4)) 190 | 191 | 192 | 193 | 194 | 195 | ## [0.4.5](https://github.com/jquense/react-common-hooks/compare/v0.4.4...v0.4.5) (2021-12-10) 196 | 197 | 198 | ### Features 199 | 200 | * add useUpdateLayoutEffect ([1914f7e](https://github.com/jquense/react-common-hooks/commit/1914f7e55449505e88e8e93a06ad71bd58b7e9a4)) 201 | 202 | 203 | 204 | 205 | 206 | ## [0.4.4](https://github.com/jquense/react-common-hooks/compare/v0.4.3...v0.4.4) (2021-12-07) 207 | 208 | 209 | ### Bug Fixes 210 | 211 | * use targetWindow ([6c8fc0d](https://github.com/jquense/react-common-hooks/commit/6c8fc0d05ee94eda479dc11b4d1ee6ed4dbf8fc2)) 212 | 213 | 214 | 215 | 216 | 217 | ## [0.4.3](https://github.com/jquense/react-common-hooks/compare/v0.4.2...v0.4.3) (2021-12-07) 218 | 219 | 220 | ### Bug Fixes 221 | 222 | * store matchers per window ([6509df8](https://github.com/jquense/react-common-hooks/commit/6509df84abf35cc20faceea3db8a37121e3b7a61)) 223 | 224 | 225 | 226 | 227 | 228 | ## [0.4.2](https://github.com/jquense/react-common-hooks/compare/v0.4.1...v0.4.2) (2021-12-07) 229 | 230 | 231 | ### Bug Fixes 232 | 233 | * minor typo in useCommittedRef ([#59](https://github.com/jquense/react-common-hooks/issues/59)) ([0ceb834](https://github.com/jquense/react-common-hooks/commit/0ceb8347527e46dba115c62578a0485da8f46e22)) 234 | 235 | 236 | ### Features 237 | 238 | * allow specifying window with useMedia/useBreakpoint ([#61](https://github.com/jquense/react-common-hooks/issues/61)) ([bf9716d](https://github.com/jquense/react-common-hooks/commit/bf9716da21aff239baf02da11671cb5caf081ccf)) 239 | 240 | 241 | 242 | 243 | 244 | ## [0.4.1](https://github.com/jquense/react-common-hooks/compare/v0.4.0...v0.4.1) (2021-09-27) 245 | 246 | 247 | ### Bug Fixes 248 | 249 | * **useFocusManager:** memo returned handlers ([410bd5a](https://github.com/jquense/react-common-hooks/commit/410bd5a10588754319a8d0a8717399aca85b1947)) 250 | 251 | 252 | ### Features 253 | 254 | * add useDebouncedValue ([362dedf](https://github.com/jquense/react-common-hooks/commit/362dedf9ace6244b4766a0128f3afe1c1305ffb6)) 255 | 256 | 257 | 258 | 259 | 260 | # [0.4.0](https://github.com/jquense/react-common-hooks/compare/v0.3.27...v0.4.0) (2021-06-17) 261 | 262 | 263 | ### Features 264 | 265 | * **useIntersectionObserver:** allow lazy roots ([4c8b77a](https://github.com/jquense/react-common-hooks/commit/4c8b77acdabeef2e2eef199e0f418ee4117b6d08)) 266 | * **useIntersectionObserver:** allow lazy roots ([#53](https://github.com/jquense/react-common-hooks/issues/53)) ([6b035cf](https://github.com/jquense/react-common-hooks/commit/6b035cf74868b381e23e69a144fd60ce06f72264)) 267 | 268 | 269 | ### BREAKING CHANGES 270 | 271 | * **useIntersectionObserver:** `null` in the native API means "the window", this is a departure to allow a consumer to hold off setting up the observer until the have a ref to the root. This was possible before by explicitly setting element to `null` until the root is available but still created an extra observer 272 | 273 | 274 | 275 | 276 | 277 | ## [0.3.27](https://github.com/jquense/react-common-hooks/compare/v0.3.26...v0.3.27) (2021-06-16) 278 | 279 | 280 | ### Bug Fixes 281 | 282 | * **types:** fix ts errors ([#47](https://github.com/jquense/react-common-hooks/issues/47)) ([310e1ec](https://github.com/jquense/react-common-hooks/commit/310e1ec605835f245e0a6fa65c5ea9ce3a07d5ae)) 283 | 284 | 285 | ### Features 286 | 287 | * **useIntersectionObserver:** add more performant overload ([6c532d8](https://github.com/jquense/react-common-hooks/commit/6c532d8e1033579f23a6fec31c98be374d8ab4e1)) 288 | * **useMutationObserver:** add overload to return records and trigger updates ([1eaa621](https://github.com/jquense/react-common-hooks/commit/1eaa621a5da1545345a7c8df79ca09ec16dcf435)) 289 | * **useToggleState:** add useToggleState hook ([#48](https://github.com/jquense/react-common-hooks/issues/48)) ([5f95dd0](https://github.com/jquense/react-common-hooks/commit/5f95dd07dde261f73b8ff658f54dcae9b6034321)) 290 | * add useDebouncedCallback hook ([05fb8da](https://github.com/jquense/react-common-hooks/commit/05fb8da28ddd8c3afdcdd5e2250b16b539edc4a3)) 291 | * add useDebouncedState hook ([a87dd3b](https://github.com/jquense/react-common-hooks/commit/a87dd3b9788102106869bcada7b87dcf9eb984d2)) 292 | * remove lodash ([#35](https://github.com/jquense/react-common-hooks/issues/35)) ([fefa63c](https://github.com/jquense/react-common-hooks/commit/fefa63c4d2115d7880423fe5d26d5413efdc7d58)) 293 | 294 | 295 | 296 | 297 | 298 | ## [0.3.26](https://github.com/jquense/react-common-hooks/compare/v0.3.25...v0.3.26) (2021-01-05) 299 | 300 | 301 | ### Bug Fixes 302 | 303 | * Allow React 17 as peer dependency ([#42](https://github.com/jquense/react-common-hooks/issues/42)) ([d0fea56](https://github.com/jquense/react-common-hooks/commit/d0fea56dda0ea7139eca4899ce67123e68dd43d2)) 304 | 305 | 306 | 307 | 308 | 309 | ## [0.3.25](https://github.com/jquense/react-common-hooks/compare/v0.3.24...v0.3.25) (2020-05-14) 310 | 311 | 312 | ### Bug Fixes 313 | 314 | * set crossOrigin prior to src ([#31](https://github.com/jquense/react-common-hooks/issues/31)) ([1d585ec](https://github.com/jquense/react-common-hooks/commit/1d585eca5a062e70405f90fd1b4ffd469dd041be)) 315 | 316 | 317 | 318 | 319 | 320 | ## [0.3.24](https://github.com/jquense/react-common-hooks/compare/v0.3.23...v0.3.24) (2020-05-13) 321 | 322 | 323 | ### Features 324 | 325 | * add useThrottledEventHandler ([#30](https://github.com/jquense/react-common-hooks/issues/30)) ([f03d52b](https://github.com/jquense/react-common-hooks/commit/f03d52b096ed95f0614728acc243a2d0b6123970)) 326 | 327 | 328 | 329 | 330 | 331 | ## [0.3.23](https://github.com/jquense/react-common-hooks/compare/v0.3.22...v0.3.23) (2020-05-13) 332 | 333 | 334 | ### Bug Fixes 335 | 336 | * improve initial state type ([#29](https://github.com/jquense/react-common-hooks/issues/29)) ([35613e4](https://github.com/jquense/react-common-hooks/commit/35613e4aac832f9a09bd63f441f7c43a0df01b8c)) 337 | 338 | 339 | 340 | 341 | 342 | ## [0.3.22](https://github.com/jquense/react-common-hooks/compare/v0.3.21...v0.3.22) (2020-03-18) 343 | 344 | 345 | ### Features 346 | 347 | * **useImmediateUpdateEffect:** allow teardown from last effect like builtin effect hooks ([b85ad85](https://github.com/jquense/react-common-hooks/commit/b85ad852a0a9efed32183be8ce0b93386c46fe95)) 348 | * add useCustomEffect ([76b18d0](https://github.com/jquense/react-common-hooks/commit/76b18d048d70beba574c70f54684fdf0b41eb4fc)) 349 | * add useMutationObserver ([c9df89c](https://github.com/jquense/react-common-hooks/commit/c9df89cc5a1e1b83eb249e291b79a6bf3b32203b)) 350 | 351 | 352 | 353 | 354 | 355 | ## [0.3.21](https://github.com/jquense/react-common-hooks/compare/v0.3.20...v0.3.21) (2020-02-25) 356 | 357 | 358 | ### Features 359 | 360 | * add useMountEffect ([f4c4753](https://github.com/jquense/react-common-hooks/commit/f4c4753f15691b4ff6f44afd2b0a8e0e8e5b28e1)) 361 | * add useRefWithInitialValueFactory ([31188e2](https://github.com/jquense/react-common-hooks/commit/31188e28dc5b68fb6e030b855ce0491fbc0c1a70)) 362 | * add useUpdateEffect ([a0d17b1](https://github.com/jquense/react-common-hooks/commit/a0d17b1dcbb4f910468bd9ae60256318fe218ecc)) 363 | 364 | 365 | 366 | 367 | 368 | ## [0.3.20](https://github.com/jquense/react-common-hooks/compare/v0.3.19...v0.3.20) (2020-01-23) 369 | 370 | 371 | ### Features 372 | 373 | * **useTimeout:** handle large delays ([#21](https://github.com/jquense/react-common-hooks/issues/21)) ([32d6951](https://github.com/jquense/react-common-hooks/commit/32d69518295b6a620afad8c495ee357a35ffeb1f)) 374 | 375 | 376 | 377 | 378 | 379 | ## [0.3.19](https://github.com/jquense/react-common-hooks/compare/v0.3.18...v0.3.19) (2019-11-26) 380 | 381 | 382 | ### Features 383 | 384 | * Add option in useInterval to run immediately ([#15](https://github.com/jquense/react-common-hooks/issues/15)) ([899369f](https://github.com/jquense/react-common-hooks/commit/899369febaaff7f60ed449b07838ef199a35b5c1)) 385 | 386 | 387 | 388 | 389 | 390 | ## [0.3.18](https://github.com/jquense/react-common-hooks/compare/v0.3.17...v0.3.18) (2019-11-22) 391 | 392 | 393 | ### Bug Fixes 394 | 395 | * Fix React peer dependency ([ea56c3d](https://github.com/jquense/react-common-hooks/commit/ea56c3d9f37f45a85d18e7b6d4092faf459bfef8)) 396 | 397 | 398 | 399 | 400 | 401 | ## [0.3.17](https://github.com/jquense/react-common-hooks/compare/v0.3.16...v0.3.17) (2019-11-22) 402 | 403 | 404 | ### Bug Fixes 405 | 406 | * Clean up package.json ([#16](https://github.com/jquense/react-common-hooks/issues/16)) ([d167136](https://github.com/jquense/react-common-hooks/commit/d1671360eed0b8137d8ca32271bef7c498b5faef)) 407 | 408 | 409 | 410 | 411 | 412 | ## [0.3.16](https://github.com/jquense/react-common-hooks/compare/v0.3.15...v0.3.16) (2019-11-22) 413 | 414 | 415 | ### Features 416 | 417 | * add useImmediateUpdateEffect ([#14](https://github.com/jquense/react-common-hooks/issues/14)) ([b6f5870](https://github.com/jquense/react-common-hooks/commit/b6f5870)) 418 | 419 | 420 | 421 | 422 | 423 | ## [0.3.15](https://github.com/jquense/react-common-hooks/compare/v0.3.14...v0.3.15) (2019-10-23) 424 | 425 | 426 | ### Features 427 | 428 | * add useSafeState ([#13](https://github.com/jquense/react-common-hooks/issues/13)) ([88c53cb](https://github.com/jquense/react-common-hooks/commit/88c53cb)) 429 | 430 | 431 | 432 | 433 | 434 | ## [0.3.14](https://github.com/jquense/react-common-hooks/compare/v0.3.13...v0.3.14) (2019-09-20) 435 | 436 | 437 | ### Features 438 | 439 | * add useAsyncSetState ([#12](https://github.com/jquense/react-common-hooks/issues/12)) ([0db4bb4](https://github.com/jquense/react-common-hooks/commit/0db4bb4)) 440 | 441 | 442 | 443 | 444 | 445 | ## [0.3.13](https://github.com/jquense/react-common-hooks/compare/v0.3.12...v0.3.13) (2019-09-06) 446 | 447 | 448 | ### Bug Fixes 449 | 450 | * useStableMemo infinite loop ([ad6fc5a](https://github.com/jquense/react-common-hooks/commit/ad6fc5a)) 451 | 452 | 453 | 454 | 455 | 456 | ## [0.3.12](https://github.com/jquense/react-common-hooks/compare/v0.3.11...v0.3.12) (2019-08-12) 457 | 458 | 459 | ### Bug Fixes 460 | 461 | * **useMediaQuery:** matches immediately in a DOM environment ([#11](https://github.com/jquense/react-common-hooks/issues/11)) ([5d1053d](https://github.com/jquense/react-common-hooks/commit/5d1053d)) 462 | 463 | 464 | 465 | 466 | 467 | ## [0.3.11](https://github.com/jquense/react-common-hooks/compare/v0.3.10...v0.3.11) (2019-08-09) 468 | 469 | 470 | ### Features 471 | 472 | * add useMergedRef ([#9](https://github.com/jquense/react-common-hooks/issues/9)) ([83bd6e1](https://github.com/jquense/react-common-hooks/commit/83bd6e1)) 473 | 474 | 475 | 476 | 477 | 478 | ## [0.3.10](https://github.com/jquense/react-common-hooks/compare/v0.3.9...v0.3.10) (2019-08-09) 479 | 480 | 481 | ### Features 482 | 483 | * add useMap and useSet ([#8](https://github.com/jquense/react-common-hooks/issues/8)) ([c21429c](https://github.com/jquense/react-common-hooks/commit/c21429c)) 484 | 485 | 486 | 487 | 488 | 489 | ## [0.3.9](https://github.com/jquense/react-common-hooks/compare/v0.3.8...v0.3.9) (2019-08-05) 490 | 491 | 492 | ### Features 493 | 494 | * add useForceUpdate() ([ba91b5f](https://github.com/jquense/react-common-hooks/commit/ba91b5f)) 495 | 496 | 497 | 498 | 499 | 500 | ## [0.3.8](https://github.com/jquense/react-common-hooks/compare/v0.3.7...v0.3.8) (2019-07-18) 501 | 502 | 503 | ### Bug Fixes 504 | 505 | * **useInterval:** don't call callback in useEffect ([#7](https://github.com/jquense/react-common-hooks/issues/7)) ([264fbeb](https://github.com/jquense/react-common-hooks/commit/264fbeb)) 506 | 507 | 508 | 509 | 510 | 511 | ## [0.3.7](https://github.com/jquense/react-common-hooks/compare/v0.3.6...v0.3.7) (2019-07-11) 512 | 513 | 514 | ### Bug Fixes 515 | 516 | * **useIntersectionObserver:** fix internal set state call triggering warnings ([7bc1d0c](https://github.com/jquense/react-common-hooks/commit/7bc1d0c)) 517 | 518 | 519 | 520 | 521 | 522 | ## [0.3.6](https://github.com/jquense/react-common-hooks/compare/v0.3.5...v0.3.6) (2019-07-08) 523 | 524 | 525 | ### Bug Fixes 526 | 527 | * wrong useStableMemo export ([16bda32](https://github.com/jquense/react-common-hooks/commit/16bda32)) 528 | * **useAnimationFrame:** stable return value ([5cca2d4](https://github.com/jquense/react-common-hooks/commit/5cca2d4)) 529 | 530 | 531 | 532 | 533 | 534 | ## [0.3.5](https://github.com/jquense/react-common-hooks/compare/v0.3.4...v0.3.5) (2019-07-06) 535 | 536 | 537 | ### Bug Fixes 538 | 539 | * **useIntersectionObserver:** return an array consistently ([ad3bb35](https://github.com/jquense/react-common-hooks/commit/ad3bb35)) 540 | 541 | 542 | ### Features 543 | 544 | * add useMediaQuery and useBreakpoint ([0d165ec](https://github.com/jquense/react-common-hooks/commit/0d165ec)) 545 | 546 | 547 | 548 | 549 | 550 | ## [0.3.4](https://github.com/jquense/react-common-hooks/compare/v0.3.3...v0.3.4) (2019-07-04) 551 | 552 | 553 | ### Bug Fixes 554 | 555 | * **useIntersectionObserver:** make SSR compatible ([fc56257](https://github.com/jquense/react-common-hooks/commit/fc56257)) 556 | 557 | 558 | 559 | 560 | 561 | ## [0.3.3](https://github.com/jquense/react-common-hooks/compare/v0.3.2...v0.3.3) (2019-07-03) 562 | 563 | 564 | ### Bug Fixes 565 | 566 | * **useEventListener:** correct listener options type ([96503dc](https://github.com/jquense/react-common-hooks/commit/96503dc)) 567 | 568 | 569 | ### Features 570 | 571 | * **useAnimationFrame:** add the ability to not clear previous callbacks ([541b82c](https://github.com/jquense/react-common-hooks/commit/541b82c)) 572 | * **useIntersectionObserver:** add an IntersectionObserver hook ([5791b22](https://github.com/jquense/react-common-hooks/commit/5791b22)) 573 | * **useIsomorphicEffect:** add hook to avoid SSR warning with useLayoutEffect ([6e6b5fa](https://github.com/jquense/react-common-hooks/commit/6e6b5fa)) 574 | * **useResizeObserver:** use with isomorphic safe effect ([4a5d976](https://github.com/jquense/react-common-hooks/commit/4a5d976)) 575 | * **useStableMemo:** add useStableMemo for a stable...useMemo ([ff981fb](https://github.com/jquense/react-common-hooks/commit/ff981fb)) 576 | 577 | 578 | 579 | 580 | 581 | ## [0.3.2](https://github.com/jquense/react-common-hooks/compare/v0.3.1...v0.3.2) (2019-06-26) 582 | 583 | 584 | ### Features 585 | 586 | * add esm support ([#5](https://github.com/jquense/react-common-hooks/issues/5)) ([ec456c1](https://github.com/jquense/react-common-hooks/commit/ec456c1)) 587 | * useEventListener allows a function, useGlobalListener works in SSR ([c67b9bc](https://github.com/jquense/react-common-hooks/commit/c67b9bc)) 588 | 589 | 590 | 591 | 592 | 593 | ## [0.3.1](https://github.com/jquense/react-common-hooks/compare/v0.3.0...v0.3.1) (2019-06-07) 594 | 595 | 596 | ### Bug Fixes 597 | 598 | * **useMergeState:** calling setState twice ([#4](https://github.com/jquense/react-common-hooks/issues/4)) ([ec7d569](https://github.com/jquense/react-common-hooks/commit/ec7d569)) 599 | 600 | 601 | 602 | 603 | 604 | # [0.3.0](https://github.com/jquense/react-common-hooks/compare/v0.2.14...v0.3.0) (2019-05-28) 605 | 606 | 607 | ### Features 608 | 609 | * better name ([dca4d54](https://github.com/jquense/react-common-hooks/commit/dca4d54)) 610 | 611 | 612 | ### BREAKING CHANGES 613 | 614 | * useRequestAnimationFrame -> useAnimationFrame 615 | 616 | 617 | 618 | 619 | 620 | ## [0.2.14](https://github.com/jquense/react-common-hooks/compare/v0.2.13...v0.2.14) (2019-05-28) 621 | 622 | 623 | ### Features 624 | 625 | * add useRequestAnimationFrame ([5c8dff1](https://github.com/jquense/react-common-hooks/commit/5c8dff1)) 626 | 627 | 628 | 629 | 630 | 631 | ## [0.2.13](https://github.com/jquense/react-common-hooks/compare/v0.2.12...v0.2.13) (2019-05-15) 632 | 633 | 634 | ### Bug Fixes 635 | 636 | * **types:** useInterval overloads ([f592102](https://github.com/jquense/react-common-hooks/commit/f592102)) 637 | 638 | 639 | 640 | 641 | 642 | ## [0.2.12](https://github.com/jquense/react-common-hooks/compare/v0.2.11...v0.2.12) (2019-05-08) 643 | 644 | 645 | ### Bug Fixes 646 | 647 | * **types:** make useFocusManger options optional ([c1ee506](https://github.com/jquense/react-common-hooks/commit/c1ee506)) 648 | * **useMounted:** provides a stable isMounted function ([3703508](https://github.com/jquense/react-common-hooks/commit/3703508)) 649 | 650 | 651 | 652 | 653 | 654 | ## [0.2.11](https://github.com/jquense/react-common-hooks/compare/v0.2.10...v0.2.11) (2019-05-02) 655 | 656 | 657 | ### Bug Fixes 658 | 659 | * allow nullable handlers in event callback ([c423cb8](https://github.com/jquense/react-common-hooks/commit/c423cb8)) 660 | 661 | 662 | 663 | 664 | 665 | ## [0.2.10](https://github.com/jquense/react-common-hooks/compare/v0.2.9...v0.2.10) (2019-05-02) 666 | 667 | 668 | ### Features 669 | 670 | * add useFocusManager() ([1cbeb66](https://github.com/jquense/react-common-hooks/commit/1cbeb66)) 671 | * add useTimeout() ([4cb8fa1](https://github.com/jquense/react-common-hooks/commit/4cb8fa1)) 672 | * add useUpdatedRef ([79b3a49](https://github.com/jquense/react-common-hooks/commit/79b3a49)) 673 | * add useWillUnmount() ([ab3e4df](https://github.com/jquense/react-common-hooks/commit/ab3e4df)) 674 | 675 | 676 | 677 | 678 | 679 | ## [0.2.9](https://github.com/jquense/react-common-hooks/compare/v0.2.8...v0.2.9) (2019-04-22) 680 | 681 | 682 | 683 | 684 | 685 | ## [0.2.8](https://github.com/jquense/react-common-hooks/compare/v0.2.7...v0.2.8) (2019-04-22) 686 | 687 | 688 | ### Features 689 | 690 | * add useCallbackRef ([722f6f5](https://github.com/jquense/react-common-hooks/commit/722f6f5)) 691 | 692 | 693 | 694 | 695 | 696 | ## [0.2.7](https://github.com/jquense/react-common-hooks/compare/v0.2.6...v0.2.7) (2019-04-16) 697 | 698 | 699 | ### Bug Fixes 700 | 701 | * **types:** allow null or undefined for useImage ([6806fd4](https://github.com/jquense/react-common-hooks/commit/6806fd4)) 702 | * **useResizeObserver:** allow element to be null or undefined ([d441fa2](https://github.com/jquense/react-common-hooks/commit/d441fa2)) 703 | 704 | 705 | 706 | 707 | 708 | ## [0.2.6](https://github.com/jquense/react-common-hooks/compare/v0.2.5...v0.2.6) (2019-04-15) 709 | 710 | 711 | 712 | 713 | 714 | ## [0.2.5](https://github.com/jquense/react-common-hooks/compare/v0.2.3...v0.2.5) (2019-04-10) 715 | 716 | 717 | ### Features 718 | 719 | * support Images directly in useImage ([f16210b](https://github.com/jquense/react-common-hooks/commit/f16210b)) 720 | 721 | 722 | 723 | 724 | 725 | ## [0.2.3](https://github.com/jquense/react-common-hooks/compare/v0.2.2...v0.2.3) (2019-03-28) 726 | 727 | 728 | ### Features 729 | 730 | * allow return null from useMergeState updater function ([c14a4e5](https://github.com/jquense/react-common-hooks/commit/c14a4e5)) 731 | 732 | 733 | 734 | 735 | 736 | ## [0.2.2](https://github.com/jquense/react-common-hooks/compare/v0.2.1...v0.2.2) (2019-03-28) 737 | 738 | 739 | ### Bug Fixes 740 | 741 | * optional crossOrigin on useImage ([2e08a7d](https://github.com/jquense/react-common-hooks/commit/2e08a7d)) 742 | 743 | 744 | 745 | 746 | 747 | ## [0.2.1](https://github.com/jquense/react-common-hooks/compare/v0.2.0...v0.2.1) (2019-03-28) 748 | 749 | 750 | ### Features 751 | 752 | * add useImage ([5b5de01](https://github.com/jquense/react-common-hooks/commit/5b5de01)) 753 | 754 | 755 | 756 | 757 | 758 | # [0.2.0](https://github.com/jquense/react-common-hooks/compare/v0.1.6...v0.2.0) (2019-03-28) 759 | 760 | 761 | ### Bug Fixes 762 | 763 | * Target ES5 in build ([#1](https://github.com/jquense/react-common-hooks/issues/1)) ([1f49c80](https://github.com/jquense/react-common-hooks/commit/1f49c80)) 764 | 765 | 766 | ### Features 767 | 768 | * add use*Interval overloads ([94dea26](https://github.com/jquense/react-common-hooks/commit/94dea26)) 769 | * add useEventListener ([bc3f520](https://github.com/jquense/react-common-hooks/commit/bc3f520)) 770 | * useEffect over LayoutEffect ([95c3416](https://github.com/jquense/react-common-hooks/commit/95c3416)) 771 | 772 | 773 | ### BREAKING CHANGES 774 | 775 | * changed arg order around on useInterval and useRafInterval 776 | * useCommittedRef fires latter now on useEffect 777 | 778 | 779 | 780 | 781 | 782 | ## [0.1.6](https://github.com/jquense/react-common-hooks/compare/v0.1.5...v0.1.6) (2019-03-27) 783 | 784 | 785 | ### Features 786 | 787 | * better globalListener types ([b0ae1ed](https://github.com/jquense/react-common-hooks/commit/b0ae1ed)) 788 | 789 | 790 | 791 | 792 | 793 | ## [0.1.5](https://github.com/jquense/react-common-hooks/compare/v0.1.4...v0.1.5) (2019-03-26) 794 | 795 | 796 | ### Bug Fixes 797 | 798 | * remove usePrevious console.log ([aeb56d8](https://github.com/jquense/react-common-hooks/commit/aeb56d8)) 799 | 800 | 801 | 802 | 803 | 804 | ## [0.1.4](https://github.com/jquense/react-common-hooks/compare/v0.1.3...v0.1.4) (2019-03-26) 805 | 806 | 807 | ### Bug Fixes 808 | 809 | * deploy new code ([be0689c](https://github.com/jquense/react-common-hooks/commit/be0689c)) 810 | 811 | 812 | 813 | 814 | 815 | ## [0.1.3](https://github.com/jquense/react-common-hooks/compare/v0.1.1...v0.1.3) (2019-03-26) 816 | 817 | 818 | ### Features 819 | 820 | * add useMounted ([36ed090](https://github.com/jquense/react-common-hooks/commit/36ed090)) 821 | 822 | 823 | 824 | 825 | 826 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jason Quense 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @restart/hooks [![npm][npm-badge]][npm] 2 | 3 | A set of utility and general-purpose React hooks. 4 | 5 | ## Install 6 | 7 | ```sh 8 | npm install @restart/hooks 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```js 14 | import useInterval from '@restart/hooks/useInterval' 15 | 16 | useInterval(() => loop(), 300, false) 17 | ``` 18 | 19 | [npm-badge]: https://img.shields.io/npm/v/@restart/hooks.svg 20 | [npm]: https://www.npmjs.org/package/@restart/hooks 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@restart/hooks", 3 | "version": "0.6.2", 4 | "type": "module", 5 | "exports": { 6 | "./*": { 7 | "require": { 8 | "types": "./cjs/*.d.ts", 9 | "default": "./cjs/*.js" 10 | }, 11 | "import": { 12 | "types": "./lib/*.d.ts", 13 | "default": "./lib/*.js" 14 | } 15 | } 16 | }, 17 | "files": [ 18 | "cjs", 19 | "lib", 20 | "CHANGELOG.md" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/react-restart/hooks.git" 25 | }, 26 | "author": { 27 | "name": "Jason Quense", 28 | "email": "monastic.panic@gmail.com" 29 | }, 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/react-restart/hooks/issues" 33 | }, 34 | "homepage": "https://github.com/react-restart/hooks#readme", 35 | "scripts": { 36 | "bootstrap": "yarn && yarn --cwd www", 37 | "test": "vitest run --coverage", 38 | "tdd": "vitest", 39 | "build": "rimraf lib cjs && concurrently --names 'esm,cjs' 'yarn build:esm' 'yarn build:cjs' && concurrently --names 'esm types,cjs types' 'yarn build:esm:types' 'yarn build:cjs:types'", 40 | "build:esm": "babel src --env-name esm --out-dir lib --extensions '.ts' --ignore='**/*.d.ts'", 41 | "build:esm:types": "tsc -p . --emitDeclarationOnly --declaration --outDir lib", 42 | "build:cjs": "babel src --out-dir cjs --extensions '.ts' --ignore='**/*.d.ts' && echo '{\"type\": \"commonjs\"}' > cjs/package.json", 43 | "build:cjs:types": "tsc -p . --emitDeclarationOnly --declaration --outDir cjs --module commonjs --moduleResolution node", 44 | "deploy-docs": "yarn --cwd www build --prefix-paths && gh-pages -d www/public", 45 | "prepublishOnly": "yarn build", 46 | "typecheck": "tsc -p . --noEmit", 47 | "release": "rollout" 48 | }, 49 | "jest": { 50 | "preset": "@4c", 51 | "testEnvironment": "jsdom", 52 | "setupFilesAfterEnv": [ 53 | "./test/setup.js" 54 | ] 55 | }, 56 | "prettier": { 57 | "singleQuote": true, 58 | "semi": false, 59 | "trailingComma": "all" 60 | }, 61 | "publishConfig": { 62 | "access": "public" 63 | }, 64 | "release": { 65 | "conventionalCommits": true 66 | }, 67 | "peerDependencies": { 68 | "react": ">=16.8.0" 69 | }, 70 | "devDependencies": { 71 | "@4c/babel-preset": "^10.2.1", 72 | "@4c/cli": "^4.0.4", 73 | "@4c/rollout": "^4.0.2", 74 | "@4c/tsconfig": "^0.4.1", 75 | "@babel/cli": "^7.26.4", 76 | "@babel/core": "^7.26.0", 77 | "@babel/preset-typescript": "^7.26.0", 78 | "@testing-library/dom": "^10.4.0", 79 | "@testing-library/react": "^16.1.0", 80 | "@types/lodash": "^4.14.195", 81 | "@types/react": "^19.0.2", 82 | "@vitest/coverage-v8": "2.1.8", 83 | "babel-plugin-transform-rename-import": "^2.3.0", 84 | "cherry-pick": "^0.5.0", 85 | "codecov": "^3.8.3", 86 | "concurrently": "^9.1.2", 87 | "eslint": "^8.44.0", 88 | "gh-pages": "^3.1.0", 89 | "husky": "^4.3.6", 90 | "jsdom": "^25.0.1", 91 | "lint-staged": "^13.2.3", 92 | "mq-polyfill": "^1.1.8", 93 | "prettier": "^3.0.0", 94 | "react": "^19.0.0", 95 | "react-dom": "^19.0.0", 96 | "rimraf": "^5.0.1", 97 | "typescript": "^5.1.6", 98 | "vitest": "^2.1.8" 99 | }, 100 | "dependencies": { 101 | "dequal": "^2.0.3" 102 | }, 103 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 104 | } 105 | -------------------------------------------------------------------------------- /src/globals.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | ResizeObserver: ResizeObserver 3 | } 4 | 5 | /** 6 | * The ResizeObserver interface is used to observe changes to Element's content 7 | * rect. 8 | * 9 | * It is modeled after MutationObserver and IntersectionObserver. 10 | */ 11 | interface ResizeObserver { 12 | new (callback: ResizeObserverCallback) 13 | 14 | /** 15 | * Adds target to the list of observed elements. 16 | */ 17 | observe: (target: Element) => void 18 | 19 | /** 20 | * Removes target from the list of observed elements. 21 | */ 22 | unobserve: (target: Element) => void 23 | 24 | /** 25 | * Clears both the observationTargets and activeTargets lists. 26 | */ 27 | disconnect: () => void 28 | } 29 | 30 | /** 31 | * This callback delivers ResizeObserver's notifications. It is invoked by a 32 | * broadcast active observations algorithm. 33 | */ 34 | interface ResizeObserverCallback { 35 | (entries: ResizeObserverEntry[], observer: ResizeObserver): void 36 | } 37 | 38 | interface ResizeObserverEntry { 39 | /** 40 | * @param target The Element whose size has changed. 41 | */ 42 | new (target: Element) 43 | 44 | /** 45 | * The Element whose size has changed. 46 | */ 47 | readonly target: Element 48 | 49 | /** 50 | * Element's content rect when ResizeObserverCallback is invoked. 51 | */ 52 | readonly contentRect: DOMRectReadOnly 53 | } 54 | 55 | interface DOMRectReadOnly { 56 | fromRect(other: DOMRectInit | undefined): DOMRectReadOnly 57 | 58 | readonly x: number 59 | readonly y: number 60 | readonly width: number 61 | readonly height: number 62 | readonly top: number 63 | readonly right: number 64 | readonly bottom: number 65 | readonly left: number 66 | 67 | toJSON: () => any 68 | } 69 | -------------------------------------------------------------------------------- /src/useAnimationFrame.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import useMounted from './useMounted.js' 3 | 4 | export interface UseAnimationFrameReturn { 5 | cancel(): void 6 | 7 | /** 8 | * Request for the provided callback to be called on the next animation frame. 9 | * Previously registered callbacks will be cancelled 10 | */ 11 | request(callback: FrameRequestCallback): void 12 | } 13 | type AnimationFrameState = { 14 | fn: FrameRequestCallback 15 | } 16 | /** 17 | * Returns a controller object for requesting and cancelling an animation frame that is properly cleaned up 18 | * once the component unmounts. New requests cancel and replace existing ones. 19 | * 20 | * ```ts 21 | * const [style, setStyle] = useState({}); 22 | * const animationFrame = useAnimationFrame(); 23 | * 24 | * const handleMouseMove = (e) => { 25 | * animationFrame.request(() => { 26 | * setStyle({ top: e.clientY, left: e.clientY }) 27 | * }) 28 | * } 29 | * 30 | * const handleMouseUp = () => { 31 | * animationFrame.cancel() 32 | * } 33 | * 34 | * return ( 35 | *
36 | * 37 | *
38 | * ) 39 | * ``` 40 | */ 41 | export default function useAnimationFrame(): UseAnimationFrameReturn { 42 | const isMounted = useMounted() 43 | 44 | const [animationFrame, setAnimationFrameState] = 45 | useState(null) 46 | 47 | useEffect(() => { 48 | if (!animationFrame) { 49 | return 50 | } 51 | 52 | const { fn } = animationFrame 53 | const handle = requestAnimationFrame(fn) 54 | return () => { 55 | cancelAnimationFrame(handle) 56 | } 57 | }, [animationFrame]) 58 | 59 | const [returnValue] = useState(() => ({ 60 | request(callback: FrameRequestCallback) { 61 | if (!isMounted()) return 62 | setAnimationFrameState({ fn: callback }) 63 | }, 64 | cancel: () => { 65 | if (!isMounted()) return 66 | setAnimationFrameState(null) 67 | }, 68 | })) 69 | 70 | return returnValue 71 | } 72 | -------------------------------------------------------------------------------- /src/useBreakpoint.ts: -------------------------------------------------------------------------------- 1 | import useMediaQuery from './useMediaQuery.js' 2 | import { useMemo } from 'react' 3 | 4 | export type BreakpointDirection = 'up' | 'down' | true 5 | 6 | export type BreakpointMap = Partial< 7 | Record 8 | > 9 | 10 | /** 11 | * Create a responsive hook we a set of breakpoint names and widths. 12 | * You can use any valid css units as well as a numbers (for pixels). 13 | * 14 | * **NOTE:** The object key order is important! it's assumed to be in order from smallest to largest 15 | * 16 | * ```ts 17 | * const useBreakpoint = createBreakpointHook({ 18 | * xs: 0, 19 | * sm: 576, 20 | * md: 768, 21 | * lg: 992, 22 | * xl: 1200, 23 | * }) 24 | * ``` 25 | * 26 | * **Watch out!** using string values will sometimes construct media queries using css `calc()` which 27 | * is NOT supported in media queries by all browsers at the moment. use numbers for 28 | * the widest range of browser support. 29 | * 30 | * @param breakpointValues A object hash of names to breakpoint dimensions 31 | */ 32 | export function createBreakpointHook( 33 | breakpointValues: Record, 34 | ) { 35 | const names = Object.keys(breakpointValues) as TKey[] 36 | 37 | function and(query: string, next: string) { 38 | if (query === next) { 39 | return next 40 | } 41 | return query ? `${query} and ${next}` : next 42 | } 43 | 44 | function getNext(breakpoint: TKey) { 45 | return names[Math.min(names.indexOf(breakpoint) + 1, names.length - 1)] 46 | } 47 | 48 | function getMaxQuery(breakpoint: TKey) { 49 | const next = getNext(breakpoint) 50 | let value = breakpointValues[next] 51 | 52 | if (typeof value === 'number') value = `${value - 0.2}px` 53 | else value = `calc(${value} - 0.2px)` 54 | 55 | return `(max-width: ${value})` 56 | } 57 | 58 | function getMinQuery(breakpoint: TKey) { 59 | let value = breakpointValues[breakpoint] 60 | if (typeof value === 'number') { 61 | value = `${value}px` 62 | } 63 | return `(min-width: ${value})` 64 | } 65 | 66 | /** 67 | * Match a set of breakpoints 68 | * 69 | * ```tsx 70 | * const MidSizeOnly = () => { 71 | * const isMid = useBreakpoint({ lg: 'down', sm: 'up' }); 72 | * 73 | * if (isMid) return
On a Reasonable sized Screen!
74 | * return null; 75 | * } 76 | * ``` 77 | * @param breakpointMap An object map of breakpoints and directions, queries are constructed using "and" to join 78 | * breakpoints together 79 | * @param window Optionally specify the target window to match against (useful when rendering into iframes) 80 | */ 81 | function useBreakpoint( 82 | breakpointMap: BreakpointMap, 83 | window?: Window, 84 | ): boolean 85 | /** 86 | * Match a single breakpoint exactly, up, or down. 87 | * 88 | * ```tsx 89 | * const PhoneOnly = () => { 90 | * const isSmall = useBreakpoint('sm', 'down'); 91 | * 92 | * if (isSmall) return
On a Small Screen!
93 | * return null; 94 | * } 95 | * ``` 96 | * 97 | * @param breakpoint The breakpoint key 98 | * @param direction A direction 'up' for a max, 'down' for min, true to match only the breakpoint 99 | * @param window Optionally specify the target window to match against (useful when rendering into iframes) 100 | */ 101 | function useBreakpoint( 102 | breakpoint: TKey, 103 | direction?: BreakpointDirection, 104 | window?: Window, 105 | ): boolean 106 | function useBreakpoint( 107 | breakpointOrMap: TKey | BreakpointMap, 108 | direction?: BreakpointDirection | Window, 109 | window?: Window, 110 | ): boolean { 111 | let breakpointMap: BreakpointMap 112 | 113 | if (typeof breakpointOrMap === 'object') { 114 | breakpointMap = breakpointOrMap 115 | window = direction as Window 116 | direction = true 117 | } else { 118 | direction = direction || true 119 | breakpointMap = { [breakpointOrMap]: direction } as Record< 120 | TKey, 121 | BreakpointDirection 122 | > 123 | } 124 | 125 | let query = useMemo( 126 | () => 127 | Object.entries(breakpointMap).reduce((query, entry) => { 128 | const [key, direction] = entry as [TKey, BreakpointDirection] 129 | 130 | if (direction === 'up' || direction === true) { 131 | query = and(query, getMinQuery(key)) 132 | } 133 | if (direction === 'down' || direction === true) { 134 | query = and(query, getMaxQuery(key)) 135 | } 136 | 137 | return query 138 | }, ''), 139 | [JSON.stringify(breakpointMap)], 140 | ) 141 | 142 | return useMediaQuery(query, window) 143 | } 144 | 145 | return useBreakpoint 146 | } 147 | 148 | export type DefaultBreakpoints = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' 149 | export type DefaultBreakpointMap = BreakpointMap 150 | 151 | const useBreakpoint = createBreakpointHook({ 152 | xs: 0, 153 | sm: 576, 154 | md: 768, 155 | lg: 992, 156 | xl: 1200, 157 | xxl: 1400, 158 | }) 159 | 160 | export default useBreakpoint 161 | -------------------------------------------------------------------------------- /src/useCallbackRef.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | /** 4 | * A convenience hook around `useState` designed to be paired with 5 | * the component [callback ref](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs) api. 6 | * Callback refs are useful over `useRef()` when you need to respond to the ref being set 7 | * instead of lazily accessing it in an effect. 8 | * 9 | * ```ts 10 | * const [element, attachRef] = useCallbackRef() 11 | * 12 | * useEffect(() => { 13 | * if (!element) return 14 | * 15 | * const calendar = new FullCalendar.Calendar(element) 16 | * 17 | * return () => { 18 | * calendar.destroy() 19 | * } 20 | * }, [element]) 21 | * 22 | * return
23 | * ``` 24 | * 25 | * @category refs 26 | */ 27 | export default function useCallbackRef(): [ 28 | TValue | null, 29 | (ref: TValue | null) => void, 30 | ] { 31 | return useState(null) 32 | } 33 | -------------------------------------------------------------------------------- /src/useCommittedRef.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | /** 4 | * Creates a `Ref` whose value is updated in an effect, ensuring the most recent 5 | * value is the one rendered with. Generally only required for Concurrent mode usage 6 | * where previous work in `render()` may be discarded before being used. 7 | * 8 | * This is safe to access in an event handler. 9 | * 10 | * @param value The `Ref` value 11 | */ 12 | function useCommittedRef( 13 | value: TValue, 14 | ): React.MutableRefObject { 15 | const ref = useRef(value) 16 | useEffect(() => { 17 | ref.current = value 18 | }, [value]) 19 | return ref 20 | } 21 | 22 | export default useCommittedRef 23 | -------------------------------------------------------------------------------- /src/useCustomEffect.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DependencyList, 3 | EffectCallback, 4 | useRef, 5 | useEffect, 6 | useDebugValue, 7 | } from 'react' 8 | import useMounted from './useMounted.js' 9 | 10 | export type EffectHook = (effect: EffectCallback, deps?: DependencyList) => void 11 | 12 | export type IsEqual = ( 13 | nextDeps: TDeps, 14 | prevDeps: TDeps, 15 | ) => boolean 16 | 17 | export type CustomEffectOptions = { 18 | isEqual: IsEqual 19 | effectHook?: EffectHook 20 | } 21 | 22 | type CleanUp = { 23 | (): void 24 | cleanup?: ReturnType 25 | } 26 | 27 | /** 28 | * a useEffect() hook with customized depedency comparision 29 | * 30 | * @param effect The effect callback 31 | * @param dependencies A list of dependencies 32 | * @param isEqual A function comparing the next and previous dependencyLists 33 | */ 34 | function useCustomEffect( 35 | effect: EffectCallback, 36 | dependencies: TDeps, 37 | isEqual: IsEqual, 38 | ): void 39 | /** 40 | * a useEffect() hook with customized depedency comparision 41 | * 42 | * @param effect The effect callback 43 | * @param dependencies A list of dependencies 44 | * @param options 45 | * @param options.isEqual A function comparing the next and previous dependencyLists 46 | * @param options.effectHook the underlying effect hook used, defaults to useEffect 47 | */ 48 | function useCustomEffect( 49 | effect: EffectCallback, 50 | dependencies: TDeps, 51 | options: CustomEffectOptions, 52 | ): void 53 | function useCustomEffect( 54 | effect: EffectCallback, 55 | dependencies: TDeps, 56 | isEqualOrOptions: IsEqual | CustomEffectOptions, 57 | ) { 58 | const isMounted = useMounted() 59 | const { isEqual, effectHook = useEffect } = 60 | typeof isEqualOrOptions === 'function' 61 | ? { isEqual: isEqualOrOptions } 62 | : isEqualOrOptions 63 | 64 | const dependenciesRef = useRef(null) 65 | dependenciesRef.current = dependencies 66 | 67 | const cleanupRef = useRef(null) 68 | 69 | effectHook(() => { 70 | // If the ref the is `null` it's either the first effect or the last effect 71 | // ran and was cleared, meaning _this_ update should run, b/c the equality 72 | // check failed on in the cleanup of the last effect. 73 | if (cleanupRef.current === null) { 74 | const cleanup = effect() 75 | 76 | cleanupRef.current = () => { 77 | if (isMounted() && isEqual(dependenciesRef.current!, dependencies)) { 78 | return 79 | } 80 | 81 | cleanupRef.current = null 82 | if (cleanup) cleanup() 83 | } 84 | } 85 | 86 | return cleanupRef.current 87 | }) 88 | 89 | useDebugValue(effect) 90 | } 91 | 92 | export default useCustomEffect 93 | -------------------------------------------------------------------------------- /src/useDebouncedCallback.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useRef } from 'react' 2 | import useTimeout from './useTimeout.js' 3 | import useEventCallback from './useEventCallback.js' 4 | 5 | export interface UseDebouncedCallbackOptions { 6 | wait: number 7 | leading?: boolean 8 | trailing?: boolean 9 | maxWait?: number 10 | } 11 | 12 | export interface UseDebouncedCallbackOptionsLeading 13 | extends UseDebouncedCallbackOptions { 14 | leading: true 15 | } 16 | 17 | const EMPTY: unique symbol = Symbol('EMPTY') 18 | 19 | /** 20 | * Creates a debounced function that will invoke the input function after the 21 | * specified wait. 22 | * 23 | * > Heads up! debounced functions are not pure since they are called in a timeout 24 | * > Don't call them inside render. 25 | * 26 | * @param fn a function that will be debounced 27 | * @param waitOrOptions a wait in milliseconds or a debounce configuration 28 | */ 29 | function useDebouncedCallback any>( 30 | fn: TCallback, 31 | options: UseDebouncedCallbackOptionsLeading, 32 | ): (...args: Parameters) => ReturnType 33 | 34 | /** 35 | * Creates a debounced function that will invoke the input function after the 36 | * specified wait. 37 | * 38 | * > Heads up! debounced functions are not pure since they are called in a timeout 39 | * > Don't call them inside render. 40 | * 41 | * @param fn a function that will be debounced 42 | * @param waitOrOptions a wait in milliseconds or a debounce configuration 43 | */ 44 | function useDebouncedCallback any>( 45 | fn: TCallback, 46 | waitOrOptions: number | UseDebouncedCallbackOptions, 47 | ): (...args: Parameters) => ReturnType | undefined 48 | 49 | function useDebouncedCallback any>( 50 | fn: TCallback, 51 | waitOrOptions: number | UseDebouncedCallbackOptions, 52 | ): (...args: Parameters) => ReturnType | undefined { 53 | const lastCallTimeRef = useRef(null) 54 | const lastInvokeTimeRef = useRef(0) 55 | const returnValueRef = useRef | typeof EMPTY>(EMPTY) 56 | 57 | const isTimerSetRef = useRef(false) 58 | const lastArgsRef = useRef(null) 59 | 60 | const handleCallback = useEventCallback(fn) 61 | 62 | const { 63 | wait, 64 | maxWait, 65 | leading = false, 66 | trailing = true, 67 | } = typeof waitOrOptions === 'number' 68 | ? ({ wait: waitOrOptions } as UseDebouncedCallbackOptions) 69 | : waitOrOptions 70 | 71 | const timeout = useTimeout() 72 | 73 | return useMemo(() => { 74 | const hasMaxWait = !!maxWait 75 | 76 | function leadingEdge(time: number) { 77 | // Reset any `maxWait` timer. 78 | lastInvokeTimeRef.current = time 79 | 80 | // Start the timer for the trailing edge. 81 | isTimerSetRef.current = true 82 | timeout.set(timerExpired, wait) 83 | 84 | if (!leading) { 85 | return returnValueRef.current === EMPTY 86 | ? undefined 87 | : returnValueRef.current 88 | } 89 | 90 | return invokeFunc(time) 91 | } 92 | 93 | function trailingEdge(time: number) { 94 | isTimerSetRef.current = false 95 | 96 | // Only invoke if we have `lastArgs` which means `func` has been 97 | // debounced at least once. 98 | if (trailing && lastArgsRef.current) { 99 | return invokeFunc(time) 100 | } 101 | 102 | lastArgsRef.current = null 103 | return returnValueRef.current === EMPTY 104 | ? undefined 105 | : returnValueRef.current 106 | } 107 | 108 | function timerExpired() { 109 | var time = Date.now() 110 | 111 | if (shouldInvoke(time)) { 112 | return trailingEdge(time) 113 | } 114 | 115 | const timeSinceLastCall = time - (lastCallTimeRef.current ?? 0) 116 | const timeSinceLastInvoke = time - lastInvokeTimeRef.current 117 | const timeWaiting = wait - timeSinceLastCall 118 | 119 | // console.log('g', Math.min(timeWaiting, maxWait - timeSinceLastInvoke)) 120 | 121 | // Restart the timer. 122 | timeout.set( 123 | timerExpired, 124 | hasMaxWait 125 | ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) 126 | : timeWaiting, 127 | ) 128 | } 129 | 130 | function invokeFunc(time: number) { 131 | const args = lastArgsRef.current ?? [] 132 | 133 | lastArgsRef.current = null 134 | lastInvokeTimeRef.current = time 135 | 136 | const retValue = handleCallback(...args) 137 | returnValueRef.current = retValue 138 | return retValue 139 | } 140 | 141 | function shouldInvoke(time: number) { 142 | const timeSinceLastCall = time - (lastCallTimeRef.current ?? 0) 143 | const timeSinceLastInvoke = time - lastInvokeTimeRef.current 144 | 145 | // Either this is the first call, activity has stopped and we're at the 146 | // trailing edge, the system time has gone backwards and we're treating 147 | // it as the trailing edge, or we've hit the `maxWait` limit. 148 | return ( 149 | lastCallTimeRef.current === null || 150 | timeSinceLastCall >= wait || 151 | timeSinceLastCall < 0 || 152 | (hasMaxWait && timeSinceLastInvoke >= maxWait) 153 | ) 154 | } 155 | 156 | return (...args: any[]) => { 157 | const time = Date.now() 158 | const isInvoking = shouldInvoke(time) 159 | 160 | lastArgsRef.current = args 161 | lastCallTimeRef.current = time 162 | 163 | if (isInvoking) { 164 | if (!isTimerSetRef.current) { 165 | return leadingEdge(lastCallTimeRef.current) 166 | } 167 | 168 | if (hasMaxWait) { 169 | // Handle invocations in a tight loop. 170 | isTimerSetRef.current = true 171 | timeout.set(timerExpired, wait) 172 | return invokeFunc(lastCallTimeRef.current) 173 | } 174 | } 175 | 176 | if (!isTimerSetRef.current) { 177 | isTimerSetRef.current = true 178 | timeout.set(timerExpired, wait) 179 | } 180 | 181 | return returnValueRef.current === EMPTY 182 | ? undefined 183 | : returnValueRef.current 184 | } 185 | }, [handleCallback, wait, maxWait, leading, trailing]) 186 | } 187 | 188 | export default useDebouncedCallback 189 | -------------------------------------------------------------------------------- /src/useDebouncedState.ts: -------------------------------------------------------------------------------- 1 | import { useState, Dispatch, SetStateAction } from 'react' 2 | import useDebouncedCallback, { 3 | UseDebouncedCallbackOptions, 4 | } from './useDebouncedCallback.js' 5 | 6 | /** 7 | * Similar to `useState`, except the setter function is debounced by 8 | * the specified delay. Unlike `useState`, the returned setter is not "pure" having 9 | * the side effect of scheduling an update in a timeout, which makes it unsafe to call 10 | * inside of the component render phase. 11 | * 12 | * ```ts 13 | * const [value, setValue] = useDebouncedState('test', 500) 14 | * 15 | * setValue('test2') 16 | * ``` 17 | * 18 | * @param initialState initial state value 19 | * @param delayOrOptions The milliseconds delay before a new value is set, or options object 20 | */ 21 | export default function useDebouncedState( 22 | initialState: T | (() => T), 23 | delayOrOptions: number | UseDebouncedCallbackOptions, 24 | ): [T, Dispatch>] { 25 | const [state, setState] = useState(initialState) 26 | 27 | const debouncedSetState = useDebouncedCallback>>( 28 | setState, 29 | delayOrOptions, 30 | ) 31 | return [state, debouncedSetState] 32 | } 33 | -------------------------------------------------------------------------------- /src/useDebouncedValue.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useDebugValue, useRef } from 'react' 2 | import useDebouncedState from './useDebouncedState.js' 3 | import { UseDebouncedCallbackOptions } from './useDebouncedCallback.js' 4 | 5 | const defaultIsEqual = (a: any, b: any) => a === b 6 | 7 | export type UseDebouncedValueOptions = UseDebouncedCallbackOptions & { 8 | isEqual?: (a: any, b: any) => boolean 9 | } 10 | 11 | /** 12 | * Debounce a value change by a specified number of milliseconds. Useful 13 | * when you want need to trigger a change based on a value change, but want 14 | * to defer changes until the changes reach some level of infrequency. 15 | * 16 | * @param value 17 | * @param waitOrOptions 18 | * @returns 19 | */ 20 | function useDebouncedValue( 21 | value: TValue, 22 | waitOrOptions: number | UseDebouncedValueOptions = 500, 23 | ): TValue { 24 | const previousValueRef = useRef(value) 25 | 26 | const isEqual = 27 | typeof waitOrOptions === 'object' 28 | ? waitOrOptions.isEqual || defaultIsEqual 29 | : defaultIsEqual 30 | 31 | const [debouncedValue, setDebouncedValue] = useDebouncedState( 32 | value, 33 | waitOrOptions, 34 | ) 35 | 36 | useDebugValue(debouncedValue) 37 | 38 | useEffect(() => { 39 | if (!isEqual || !isEqual(previousValueRef.current, value)) { 40 | previousValueRef.current = value 41 | setDebouncedValue(value) 42 | } 43 | }) 44 | 45 | return debouncedValue 46 | } 47 | 48 | export default useDebouncedValue 49 | -------------------------------------------------------------------------------- /src/useEventCallback.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useCallback, useRef } from 'react' 2 | import useCommittedRef from './useCommittedRef.js' 3 | 4 | export default function useEventCallback< 5 | TCallback extends (...args: any[]) => any, 6 | >(fn?: TCallback | null): TCallback { 7 | const ref = useCommittedRef(fn) 8 | return useCallback( 9 | function (...args: any[]) { 10 | return ref.current && ref.current(...args) 11 | }, 12 | [ref], 13 | ) as any 14 | } 15 | -------------------------------------------------------------------------------- /src/useEventListener.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | import useEventCallback from './useEventCallback.js' 4 | 5 | type EventHandler = ( 6 | this: T, 7 | ev: DocumentEventMap[K], 8 | ) => any 9 | 10 | /** 11 | * Attaches an event handler outside directly to specified DOM element 12 | * bypassing the react synthetic event system. 13 | * 14 | * @param element The target to listen for events on 15 | * @param event The DOM event name 16 | * @param handler An event handler 17 | * @param capture Whether or not to listen during the capture event phase 18 | */ 19 | export default function useEventListener< 20 | T extends Element | Document | Window, 21 | K extends keyof DocumentEventMap, 22 | >( 23 | eventTarget: T | (() => T), 24 | event: K, 25 | listener: EventHandler, 26 | capture: boolean | AddEventListenerOptions = false, 27 | ) { 28 | const handler = useEventCallback(listener) as EventListener 29 | 30 | useEffect(() => { 31 | const target = 32 | typeof eventTarget === 'function' ? eventTarget() : eventTarget 33 | 34 | target.addEventListener(event, handler, capture) 35 | return () => target.removeEventListener(event, handler, capture) 36 | }, [eventTarget]) 37 | } 38 | -------------------------------------------------------------------------------- /src/useFocusManager.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useRef } from 'react' 2 | import useEventCallback from './useEventCallback.js' 3 | import useMounted from './useMounted.js' 4 | 5 | export interface FocusManagerOptions { 6 | /** 7 | * A callback fired when focus shifts. returning `false` will prevent 8 | * handling the focus event 9 | */ 10 | willHandle?(focused: boolean, event: React.FocusEvent): boolean | void 11 | 12 | /** 13 | * A callback fired after focus is handled but before onChange is called 14 | */ 15 | didHandle?(focused: boolean, event: React.FocusEvent): void 16 | 17 | /** 18 | * A callback fired after focus has changed 19 | */ 20 | onChange?(focused: boolean, event: React.FocusEvent): void 21 | 22 | /** 23 | * When true, the event handlers will not report focus changes 24 | */ 25 | isDisabled: () => boolean 26 | } 27 | 28 | export interface FocusController { 29 | onBlur: (event: any) => void 30 | onFocus: (event: any) => void 31 | } 32 | /** 33 | * useFocusManager provides a way to track and manage focus as it moves around 34 | * a container element. An `onChange` is fired when focus enters or leaves the 35 | * element, but not when it moves around inside the element, similar to 36 | * `pointerenter` and `pointerleave` DOM events. 37 | * 38 | * ```tsx 39 | * const [focused, setFocusState] = useState(false) 40 | * 41 | * const { onBlur, onFocus } = useFocusManager({ 42 | * onChange: nextFocused => setFocusState(nextFocused) 43 | * }) 44 | * 45 | * return ( 46 | *
47 | * {String(focused)} 48 | * 49 | * 50 | * 51 | * 52 | *
53 | * ``` 54 | * 55 | * @returns a memoized FocusController containing event handlers 56 | */ 57 | export default function useFocusManager( 58 | opts: FocusManagerOptions, 59 | ): FocusController { 60 | const isMounted = useMounted() 61 | 62 | const lastFocused = useRef(undefined) 63 | const handle = useRef(undefined) 64 | 65 | const willHandle = useEventCallback(opts.willHandle) 66 | const didHandle = useEventCallback(opts.didHandle) 67 | const onChange = useEventCallback(opts.onChange) 68 | const isDisabled = useEventCallback(opts.isDisabled) 69 | 70 | const handleChange = useCallback( 71 | (focused: boolean, event: React.FocusEvent) => { 72 | if (focused !== lastFocused.current) { 73 | didHandle?.(focused, event) 74 | 75 | // only fire a change when unmounted if its a blur 76 | if (isMounted() || !focused) { 77 | lastFocused.current = focused 78 | onChange?.(focused, event) 79 | } 80 | } 81 | }, 82 | [isMounted, didHandle, onChange, lastFocused], 83 | ) 84 | 85 | const handleFocusChange = useCallback( 86 | (focused: boolean, event: React.FocusEvent) => { 87 | if (isDisabled()) return 88 | if (event && event.persist) event.persist() 89 | 90 | if (willHandle?.(focused, event) === false) { 91 | return 92 | } 93 | clearTimeout(handle.current) 94 | 95 | if (focused) { 96 | handleChange(focused, event) 97 | } else { 98 | handle.current = window.setTimeout(() => handleChange(focused, event)) 99 | } 100 | }, 101 | [willHandle, handleChange], 102 | ) 103 | 104 | return useMemo( 105 | () => ({ 106 | onBlur: (event: React.FocusEvent) => { 107 | handleFocusChange(false, event) 108 | }, 109 | onFocus: (event: React.FocusEvent) => { 110 | handleFocusChange(true, event) 111 | }, 112 | }), 113 | [handleFocusChange], 114 | ) 115 | } 116 | -------------------------------------------------------------------------------- /src/useForceUpdate.ts: -------------------------------------------------------------------------------- 1 | import { useReducer } from 'react' 2 | 3 | /** 4 | * Returns a function that triggers a component update. the hook equivalent to 5 | * `this.forceUpdate()` in a class component. In most cases using a state value directly 6 | * is preferable but may be required in some advanced usages of refs for interop or 7 | * when direct DOM manipulation is required. 8 | * 9 | * ```ts 10 | * const forceUpdate = useForceUpdate(); 11 | * 12 | * const updateOnClick = useCallback(() => { 13 | * forceUpdate() 14 | * }, [forceUpdate]) 15 | * 16 | * return 17 | * ``` 18 | */ 19 | export default function useForceUpdate(): () => void { 20 | // The toggling state value is designed to defeat React optimizations for skipping 21 | // updates when they are strictly equal to the last state value 22 | const [, dispatch] = useReducer((revision) => revision + 1, 0) 23 | return dispatch as () => void 24 | } 25 | -------------------------------------------------------------------------------- /src/useGlobalListener.ts: -------------------------------------------------------------------------------- 1 | import useEventListener from './useEventListener.js' 2 | import { useCallback } from 'react' 3 | 4 | type DocumentEventHandler = ( 5 | this: Document, 6 | ev: DocumentEventMap[K], 7 | ) => any 8 | 9 | /** 10 | * Attaches an event handler outside directly to the `document`, 11 | * bypassing the react synthetic event system. 12 | * 13 | * ```ts 14 | * useGlobalListener('keydown', (event) => { 15 | * console.log(event.key) 16 | * }) 17 | * ``` 18 | * 19 | * @param event The DOM event name 20 | * @param handler An event handler 21 | * @param capture Whether or not to listen during the capture event phase 22 | */ 23 | export default function useGlobalListener( 24 | event: K, 25 | handler: DocumentEventHandler, 26 | capture: boolean | AddEventListenerOptions = false, 27 | ) { 28 | const documentTarget = useCallback(() => document, []) 29 | 30 | return useEventListener(documentTarget, event, handler, capture) 31 | } 32 | -------------------------------------------------------------------------------- /src/useImage.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | type State = { 4 | image: HTMLImageElement | null 5 | error: unknown | null 6 | } 7 | 8 | /** 9 | * Fetch and load an image for programatic use such as in a `` element. 10 | * 11 | * @param imageOrUrl The `HtmlImageElement` or image url to load 12 | * @param crossOrigin The `crossorigin` attribute to set 13 | * 14 | * ```ts 15 | * const { image, error } = useImage('/static/kittens.png') 16 | * const ref = useRef() 17 | * 18 | * useEffect(() => { 19 | * const ctx = ref.current.getContext('2d') 20 | * 21 | * if (image) { 22 | * ctx.drawImage(image, 0, 0) 23 | * } 24 | * }, [ref, image]) 25 | * 26 | * return ( 27 | * <> 28 | * {error && "there was a problem loading the image"} 29 | * 30 | * 31 | * ``` 32 | */ 33 | export default function useImage( 34 | imageOrUrl?: string | HTMLImageElement | null | undefined, 35 | crossOrigin?: 'anonymous' | 'use-credentials' | string, 36 | ) { 37 | const [state, setState] = useState({ 38 | image: null, 39 | error: null, 40 | }) 41 | 42 | useEffect(() => { 43 | if (!imageOrUrl) return undefined 44 | 45 | let image: HTMLImageElement 46 | 47 | if (typeof imageOrUrl === 'string') { 48 | image = new Image() 49 | if (crossOrigin) image.crossOrigin = crossOrigin 50 | image.src = imageOrUrl 51 | } else { 52 | image = imageOrUrl 53 | 54 | if (image.complete && image.naturalHeight > 0) { 55 | setState({ image, error: null }) 56 | return 57 | } 58 | } 59 | 60 | function onLoad() { 61 | setState({ image, error: null }) 62 | } 63 | 64 | function onError(error: ErrorEvent) { 65 | setState({ image, error }) 66 | } 67 | 68 | image.addEventListener('load', onLoad) 69 | image.addEventListener('error', onError) 70 | 71 | return () => { 72 | image.removeEventListener('load', onLoad) 73 | image.removeEventListener('error', onError) 74 | } 75 | }, [imageOrUrl, crossOrigin]) 76 | 77 | return state 78 | } 79 | -------------------------------------------------------------------------------- /src/useImmediateUpdateEffect.ts: -------------------------------------------------------------------------------- 1 | import useUpdateImmediateEffect from './useUpdateImmediateEffect.js' 2 | 3 | export default useUpdateImmediateEffect 4 | -------------------------------------------------------------------------------- /src/useIntersectionObserver.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import useStableMemo from './useStableMemo.js' 4 | import useEffect from './useIsomorphicEffect.js' 5 | import useEventCallback from './useEventCallback.js' 6 | 7 | /** 8 | * Setup an [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) on 9 | * a DOM Element that returns it's entries as they arrive. 10 | * 11 | * @param element The DOM element to observe 12 | * @param init IntersectionObserver options with a notable change, 13 | * unlike a plain IntersectionObserver `root: null` means "not provided YET", 14 | * and the hook will wait until it receives a non-null value to set up the observer. 15 | * This change allows for easier syncing of element and root values in a React 16 | * context. 17 | */ 18 | function useIntersectionObserver( 19 | element: TElement | null | undefined, 20 | options?: IntersectionObserverInit, 21 | ): IntersectionObserverEntry[] 22 | /** 23 | * Setup an [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) on 24 | * a DOM Element. This overload does not trigger component updates when receiving new 25 | * entries. This allows for finer grained performance optimizations by the consumer. 26 | * 27 | * @param element The DOM element to observe 28 | * @param callback A listener for intersection updates. 29 | * @param init IntersectionObserver options with a notable change, 30 | * unlike a plain IntersectionObserver `root: null` means "not provided YET", 31 | * and the hook will wait until it receives a non-null value to set up the observer. 32 | * This change allows for easier syncing of element and root values in a React 33 | * context. 34 | * 35 | */ 36 | function useIntersectionObserver( 37 | element: TElement | null | undefined, 38 | callback: IntersectionObserverCallback, 39 | options?: IntersectionObserverInit, 40 | ): void 41 | function useIntersectionObserver( 42 | element: TElement | null | undefined, 43 | callbackOrOptions?: IntersectionObserverCallback | IntersectionObserverInit, 44 | maybeOptions?: IntersectionObserverInit, 45 | ): void | IntersectionObserverEntry[] { 46 | let callback: IntersectionObserverCallback | undefined 47 | let options: IntersectionObserverInit 48 | if (typeof callbackOrOptions === 'function') { 49 | callback = callbackOrOptions 50 | options = maybeOptions || {} 51 | } else { 52 | options = callbackOrOptions || {} 53 | } 54 | const { threshold, root, rootMargin } = options 55 | const [entries, setEntry] = useState(null) 56 | 57 | const handler = useEventCallback(callback || setEntry) 58 | 59 | // We wait for element to exist before constructing 60 | const observer = useStableMemo( 61 | () => 62 | root !== null && 63 | typeof IntersectionObserver !== 'undefined' && 64 | new IntersectionObserver(handler, { 65 | threshold, 66 | root, 67 | rootMargin, 68 | }), 69 | 70 | [handler, root, rootMargin, threshold && JSON.stringify(threshold)], 71 | ) 72 | 73 | useEffect(() => { 74 | if (!element || !observer) return 75 | 76 | observer.observe(element) 77 | 78 | return () => { 79 | observer.unobserve(element) 80 | } 81 | }, [observer, element]) 82 | 83 | return callback ? undefined : entries || [] 84 | } 85 | 86 | export default useIntersectionObserver 87 | -------------------------------------------------------------------------------- /src/useInterval.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import useCommittedRef from './useCommittedRef.js' 3 | 4 | /** 5 | * Creates a `setInterval` that is properly cleaned up when a component unmounted 6 | * 7 | * ```tsx 8 | * function Timer() { 9 | * const [timer, setTimer] = useState(0) 10 | * useInterval(() => setTimer(i => i + 1), 1000) 11 | * 12 | * return {timer} seconds past 13 | * } 14 | * ``` 15 | * 16 | * @param fn an function run on each interval 17 | * @param ms The milliseconds duration of the interval 18 | */ 19 | function useInterval(fn: () => void, ms: number): void 20 | 21 | /** 22 | * Creates a pausable `setInterval` that is properly cleaned up when a component unmounted 23 | * 24 | * ```tsx 25 | * const [paused, setPaused] = useState(false) 26 | * const [timer, setTimer] = useState(0) 27 | * 28 | * useInterval(() => setTimer(i => i + 1), 1000, paused) 29 | * 30 | * return ( 31 | * 32 | * {timer} seconds past 33 | * 34 | * 35 | * 36 | * ) 37 | * ``` 38 | * 39 | * @param fn an function run on each interval 40 | * @param ms The milliseconds duration of the interval 41 | * @param paused Whether or not the interval is currently running 42 | */ 43 | function useInterval(fn: () => void, ms: number, paused: boolean): void 44 | 45 | /** 46 | * Creates a pausable `setInterval` that _fires_ immediately and is 47 | * properly cleaned up when a component unmounted 48 | * 49 | * ```tsx 50 | * const [timer, setTimer] = useState(-1) 51 | * useInterval(() => setTimer(i => i + 1), 1000, false, true) 52 | * 53 | * // will update to 0 on the first effect 54 | * return {timer} seconds past 55 | * ``` 56 | * 57 | * @param fn an function run on each interval 58 | * @param ms The milliseconds duration of the interval 59 | * @param paused Whether or not the interval is currently running 60 | * @param runImmediately Whether to run the function immediately on mount or unpause 61 | * rather than waiting for the first interval to elapse 62 | * 63 | 64 | */ 65 | function useInterval( 66 | fn: () => void, 67 | ms: number, 68 | paused: boolean, 69 | runImmediately: boolean, 70 | ): void 71 | 72 | function useInterval( 73 | fn: () => void, 74 | ms: number, 75 | paused: boolean = false, 76 | runImmediately: boolean = false, 77 | ): void { 78 | let handle: number 79 | const fnRef = useCommittedRef(fn) 80 | // this ref is necessary b/c useEffect will sometimes miss a paused toggle 81 | // orphaning a setTimeout chain in the aether, so relying on it's refresh logic is not reliable. 82 | const pausedRef = useCommittedRef(paused) 83 | const tick = () => { 84 | if (pausedRef.current) return 85 | fnRef.current() 86 | schedule() // eslint-disable-line no-use-before-define 87 | } 88 | 89 | const schedule = () => { 90 | clearTimeout(handle) 91 | handle = setTimeout(tick, ms) as any 92 | } 93 | 94 | useEffect(() => { 95 | if (runImmediately) { 96 | tick() 97 | } else { 98 | schedule() 99 | } 100 | return () => clearTimeout(handle) 101 | }, [paused, runImmediately]) 102 | } 103 | 104 | export default useInterval 105 | -------------------------------------------------------------------------------- /src/useIsInitialRenderRef.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect, useRef } from 'react' 2 | 3 | /** 4 | * Returns ref that is `true` on the initial render and `false` on subsequent renders. It 5 | * is StrictMode safe, so will reset correctly if the component is unmounted and remounted. 6 | * 7 | * This hook *must* be used before any effects that read it's value to be accurate. 8 | */ 9 | export default function useIsInitialRenderRef() { 10 | const effectCount = useRef(0) 11 | const isInitialRenderRef = useRef(true) 12 | 13 | useLayoutEffect(() => { 14 | effectCount.current += 1 15 | 16 | if (effectCount.current >= 2) { 17 | isInitialRenderRef.current = false 18 | } 19 | }) 20 | 21 | // Strict mode handling in React 18 22 | useEffect( 23 | () => () => { 24 | effectCount.current = 0 25 | isInitialRenderRef.current = true 26 | }, 27 | [], 28 | ) 29 | 30 | return isInitialRenderRef 31 | } 32 | -------------------------------------------------------------------------------- /src/useIsomorphicEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect } from 'react' 2 | 3 | const isReactNative = 4 | typeof global !== 'undefined' && 5 | // @ts-ignore 6 | global.navigator && 7 | // @ts-ignore 8 | global.navigator.product === 'ReactNative' 9 | 10 | const isDOM = typeof document !== 'undefined' 11 | 12 | /** 13 | * Is `useLayoutEffect` in a DOM or React Native environment, otherwise resolves to useEffect 14 | * Only useful to avoid the console warning. 15 | * 16 | * PREFER `useEffect` UNLESS YOU KNOW WHAT YOU ARE DOING. 17 | * 18 | * @category effects 19 | */ 20 | export default isDOM || isReactNative ? useLayoutEffect : useEffect 21 | -------------------------------------------------------------------------------- /src/useMap.ts: -------------------------------------------------------------------------------- 1 | import useForceUpdate from './useForceUpdate.js' 2 | import useStableMemo from './useStableMemo.js' 3 | 4 | export class ObservableMap extends Map { 5 | private readonly listener: (map: ObservableMap) => void 6 | 7 | constructor( 8 | listener: (map: ObservableMap) => void, 9 | init?: Iterable>, 10 | ) { 11 | super(init as any) 12 | 13 | this.listener = listener 14 | } 15 | 16 | set(key: K, value: V): this { 17 | super.set(key, value) 18 | // When initializing the Map, the base Map calls this.set() before the 19 | // listener is assigned so it will be undefined 20 | if (this.listener) this.listener(this) 21 | return this 22 | } 23 | 24 | delete(key: K): boolean { 25 | let result = super.delete(key) 26 | this.listener(this) 27 | return result 28 | } 29 | 30 | clear(): void { 31 | super.clear() 32 | this.listener(this) 33 | } 34 | } 35 | 36 | /** 37 | * Create and return a [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) that triggers rerenders when it's updated. 38 | * 39 | * ```tsx 40 | * const customerAges = useMap([ 41 | * ['john', 24], 42 | * ['betsy', 25] 43 | * ]); 44 | * 45 | * return ( 46 | * <> 47 | * {Array.from(ids, ([name, age]) => ( 48 | *
49 | * {name}: {age}. 50 | *
51 | * )} 52 | * 53 | * ) 54 | * ``` 55 | * 56 | * @param init initial Map entries 57 | */ 58 | function useMap(init?: Iterable>) { 59 | const forceUpdate = useForceUpdate() 60 | 61 | return useStableMemo(() => new ObservableMap(forceUpdate, init), []) 62 | } 63 | 64 | export default useMap 65 | -------------------------------------------------------------------------------- /src/useMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import useEffect from './useIsomorphicEffect.js' 2 | import { useState } from 'react' 3 | 4 | interface RefCountedMediaQueryList extends MediaQueryList { 5 | refCount: number 6 | } 7 | const matchersByWindow = new WeakMap< 8 | Window, 9 | Map 10 | >() 11 | 12 | const getMatcher = ( 13 | query: string | null, 14 | targetWindow?: Window, 15 | ): RefCountedMediaQueryList | undefined => { 16 | if (!query || !targetWindow) return undefined 17 | 18 | const matchers = 19 | matchersByWindow.get(targetWindow) || 20 | new Map() 21 | 22 | matchersByWindow.set(targetWindow, matchers) 23 | 24 | let mql = matchers.get(query) 25 | if (!mql) { 26 | mql = targetWindow.matchMedia(query) as RefCountedMediaQueryList 27 | mql.refCount = 0 28 | matchers.set(mql.media, mql) 29 | } 30 | return mql 31 | } 32 | /** 33 | * Match a media query and get updates as the match changes. The media string is 34 | * passed directly to `window.matchMedia` and run as a Layout Effect, so initial 35 | * matches are returned before the browser has a chance to paint. 36 | * 37 | * ```tsx 38 | * function Page() { 39 | * const isWide = useMediaQuery('min-width: 1000px') 40 | * 41 | * return isWide ? "very wide" : 'not so wide' 42 | * } 43 | * ``` 44 | * 45 | * Media query lists are also reused globally, hook calls for the same query 46 | * will only create a matcher once under the hood. 47 | * 48 | * @param query A media query 49 | * @param targetWindow The window to match against, uses the globally available one as a default. 50 | */ 51 | export default function useMediaQuery( 52 | query: string | null, 53 | targetWindow: Window | undefined = typeof window === 'undefined' 54 | ? undefined 55 | : window, 56 | ) { 57 | const mql = getMatcher(query, targetWindow) 58 | 59 | const [matches, setMatches] = useState(() => (mql ? mql.matches : false)) 60 | 61 | useEffect(() => { 62 | let mql = getMatcher(query, targetWindow) 63 | if (!mql) { 64 | return setMatches(false) 65 | } 66 | 67 | let matchers = matchersByWindow.get(targetWindow!) 68 | 69 | const handleChange = () => { 70 | setMatches(mql!.matches) 71 | } 72 | 73 | mql.refCount++ 74 | mql.addListener(handleChange) 75 | 76 | handleChange() 77 | 78 | return () => { 79 | mql!.removeListener(handleChange) 80 | mql!.refCount-- 81 | if (mql!.refCount <= 0) { 82 | matchers?.delete(mql!.media) 83 | } 84 | mql = undefined 85 | } 86 | }, [query]) 87 | 88 | return matches 89 | } 90 | -------------------------------------------------------------------------------- /src/useMergeState.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | type Updater = (state: TState) => Partial | null 4 | 5 | /** 6 | * Updates state, partial updates are merged into existing state values 7 | */ 8 | export type MergeStateSetter = ( 9 | update: Updater | Partial | null, 10 | ) => void 11 | 12 | /** 13 | * Mimics a React class component's state model, of having a single unified 14 | * `state` object and an updater that merges updates into the existing state, as 15 | * opposed to replacing it. 16 | * 17 | * ```js 18 | * const [state, setState] = useMergeState({ name: 'Betsy', age: 24 }) 19 | * 20 | * setState({ name: 'Johan' }) // { name: 'Johan', age: 24 } 21 | * 22 | * setState(state => ({ age: state.age + 10 })) // { name: 'Johan', age: 34 } 23 | * ``` 24 | * 25 | * @param initialState The initial state object 26 | */ 27 | export default function useMergeState( 28 | initialState: TState | (() => TState), 29 | ): [TState, MergeStateSetter] { 30 | const [state, setState] = useState(initialState) 31 | 32 | const updater = useCallback( 33 | (update: Updater | Partial | null) => { 34 | if (update === null) return 35 | if (typeof update === 'function') { 36 | setState(state => { 37 | const nextState = update(state) 38 | return nextState == null ? state : { ...state, ...nextState } 39 | }) 40 | } else { 41 | setState(state => ({ ...state, ...update })) 42 | } 43 | }, 44 | [setState], 45 | ) 46 | 47 | return [state, updater] 48 | } 49 | -------------------------------------------------------------------------------- /src/useMergeStateFromProps.ts: -------------------------------------------------------------------------------- 1 | import useMergeState, { MergeStateSetter } from './useMergeState.js' 2 | 3 | type Mapper = ( 4 | props: TProps, 5 | state: TState, 6 | ) => null | Partial 7 | 8 | export default function useMergeStateFromProps( 9 | props: TProps, 10 | gDSFP: Mapper, 11 | initialState: TState, 12 | ): [TState, MergeStateSetter] { 13 | const [state, setState] = useMergeState(initialState) 14 | 15 | const nextState = gDSFP(props, state) 16 | 17 | if (nextState !== null) setState(nextState) 18 | 19 | return [state, setState] 20 | } 21 | -------------------------------------------------------------------------------- /src/useMergedRefs.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | 3 | type CallbackRef = (ref: T | null) => void 4 | type Ref = React.MutableRefObject | CallbackRef 5 | 6 | function toFnRef(ref?: Ref | null) { 7 | return !ref || typeof ref === 'function' 8 | ? ref 9 | : (value: T | null) => { 10 | ref.current = value as T 11 | } 12 | } 13 | 14 | export function mergeRefs(refA?: Ref | null, refB?: Ref | null) { 15 | const a = toFnRef(refA) 16 | const b = toFnRef(refB) 17 | return (value: T | null) => { 18 | if (a) a(value) 19 | if (b) b(value) 20 | } 21 | } 22 | 23 | /** 24 | * Create and returns a single callback ref composed from two other Refs. 25 | * 26 | * ```tsx 27 | * const Button = React.forwardRef((props, ref) => { 28 | * const [element, attachRef] = useCallbackRef(); 29 | * const mergedRef = useMergedRefs(ref, attachRef); 30 | * 31 | * return 44 | *
45 | * )} 46 | * 47 | * ) 48 | * ``` 49 | * 50 | * @param init initial Set values 51 | */ 52 | function useSet(init?: Iterable): ObservableSet { 53 | const forceUpdate = useForceUpdate() 54 | return useStableMemo(() => new ObservableSet(forceUpdate, init), []) 55 | } 56 | 57 | export default useSet 58 | -------------------------------------------------------------------------------- /src/useStableMemo.ts: -------------------------------------------------------------------------------- 1 | import { DependencyList, useRef } from 'react' 2 | 3 | function isEqual(a: DependencyList, b: DependencyList) { 4 | if (a.length !== b.length) return false 5 | 6 | for (let i = 0; i < a.length; i++) { 7 | if (a[i] !== b[i]) { 8 | return false 9 | } 10 | } 11 | return true 12 | } 13 | 14 | type DepsCache = { 15 | deps?: DependencyList 16 | result: T 17 | } 18 | 19 | /** 20 | * Identical to `useMemo` _except_ that it provides a semantic guarantee that 21 | * values will not be invalidated unless the dependencies change. This is unlike 22 | * the built in `useMemo` which may discard memoized values for performance reasons. 23 | * 24 | * @param factory A function that returns a value to be memoized 25 | * @param deps A dependency array 26 | */ 27 | export default function useStableMemo( 28 | factory: () => T, 29 | deps?: DependencyList, 30 | ): T { 31 | let isValid: boolean = true 32 | 33 | const valueRef = useRef>(undefined) 34 | // initial hook call 35 | if (!valueRef.current) { 36 | valueRef.current = { 37 | deps, 38 | result: factory(), 39 | } 40 | // subsequent calls 41 | } else { 42 | isValid = !!( 43 | deps && 44 | valueRef.current.deps && 45 | isEqual(deps, valueRef.current.deps) 46 | ) 47 | } 48 | 49 | const cache = isValid ? valueRef.current : { deps, result: factory() } 50 | // must update immediately so any sync renders here don't cause an infinite loop 51 | valueRef.current = cache 52 | 53 | return cache.result 54 | } 55 | -------------------------------------------------------------------------------- /src/useStateAsync.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useRef, useState } from 'react' 2 | 3 | type Updater = (state: TState) => TState 4 | 5 | export type AsyncSetState = ( 6 | stateUpdate: React.SetStateAction, 7 | ) => Promise 8 | 9 | /** 10 | * A hook that mirrors `useState` in function and API, expect that setState 11 | * calls return a promise that resolves after the state has been set (in an effect). 12 | * 13 | * This is _similar_ to the second callback in classy setState calls, but fires later. 14 | * 15 | * ```ts 16 | * const [counter, setState] = useStateAsync(1); 17 | * 18 | * const handleIncrement = async () => { 19 | * await setState(2); 20 | * doWorkRequiringCurrentState() 21 | * } 22 | * ``` 23 | * 24 | * @param initialState initialize with some state value same as `useState` 25 | */ 26 | export default function useStateAsync( 27 | initialState: TState | (() => TState), 28 | ): [TState, AsyncSetState] { 29 | const [state, setState] = useState(initialState) 30 | const resolvers = useRef<((state: TState) => void)[]>([]) 31 | 32 | useEffect(() => { 33 | resolvers.current.forEach(resolve => resolve(state)) 34 | resolvers.current.length = 0 35 | }, [state]) 36 | 37 | const setStateAsync = useCallback( 38 | (update: Updater | TState) => { 39 | return new Promise((resolve, reject) => { 40 | setState(prevState => { 41 | try { 42 | let nextState: TState 43 | // ugly instanceof for typescript 44 | if (update instanceof Function) { 45 | nextState = update(prevState) 46 | } else { 47 | nextState = update 48 | } 49 | 50 | // If state does not change, we must resolve the promise because 51 | // react won't re-render and effect will not resolve. If there are already 52 | // resolvers queued, then it should be safe to assume an update will happen 53 | if (!resolvers.current.length && Object.is(nextState, prevState)) { 54 | resolve(nextState) 55 | } else { 56 | resolvers.current.push(resolve) 57 | } 58 | return nextState 59 | } catch (e) { 60 | reject(e) 61 | throw e 62 | } 63 | }) 64 | }) 65 | }, 66 | [setState], 67 | ) 68 | return [state, setStateAsync] 69 | } 70 | -------------------------------------------------------------------------------- /src/useThrottledEventHandler.ts: -------------------------------------------------------------------------------- 1 | import { useRef, SyntheticEvent } from 'react' 2 | import useMounted from './useMounted.js' 3 | import useEventCallback from './useEventCallback.js' 4 | 5 | const isSyntheticEvent = (event: any): event is SyntheticEvent => 6 | typeof event.persist === 'function' 7 | 8 | export type ThrottledHandler = ((event: TEvent) => void) & { 9 | clear(): void 10 | } 11 | 12 | /** 13 | * Creates a event handler function throttled by `requestAnimationFrame` that 14 | * returns the **most recent** event. Useful for noisy events that update react state. 15 | * 16 | * ```tsx 17 | * function Component() { 18 | * const [position, setPosition] = useState(); 19 | * const handleMove = useThrottledEventHandler( 20 | * (event) => { 21 | * setPosition({ 22 | * top: event.clientX, 23 | * left: event.clientY, 24 | * }) 25 | * } 26 | * ) 27 | * 28 | * return ( 29 | *
30 | *
31 | *
32 | * ); 33 | * } 34 | * ``` 35 | * 36 | * @param handler An event handler function 37 | * @typeParam TEvent The event object passed to the handler function 38 | * @returns The event handler with a `clear` method attached for clearing any in-flight handler calls 39 | * 40 | */ 41 | export default function useThrottledEventHandler< 42 | TEvent extends object = SyntheticEvent, 43 | >(handler: (event: TEvent) => void): ThrottledHandler { 44 | const isMounted = useMounted() 45 | const eventHandler = useEventCallback(handler) 46 | 47 | const nextEventInfoRef = useRef<{ 48 | event: TEvent | null 49 | handle: null | number 50 | }>({ 51 | event: null, 52 | handle: null, 53 | }) 54 | 55 | const clear = () => { 56 | cancelAnimationFrame(nextEventInfoRef.current.handle!) 57 | nextEventInfoRef.current.handle = null 58 | } 59 | 60 | const handlePointerMoveAnimation = () => { 61 | const { current: next } = nextEventInfoRef 62 | 63 | if (next.handle && next.event) { 64 | if (isMounted()) { 65 | next.handle = null 66 | eventHandler(next.event) 67 | } 68 | } 69 | next.event = null 70 | } 71 | 72 | const throttledHandler = (event: TEvent) => { 73 | if (!isMounted()) return 74 | 75 | if (isSyntheticEvent(event)) { 76 | event.persist() 77 | } 78 | // Special handling for a React.Konva event which reuses the 79 | // event object as it bubbles, setting target 80 | else if ('evt' in event) { 81 | event = { ...event } 82 | } 83 | 84 | nextEventInfoRef.current.event = event 85 | if (!nextEventInfoRef.current.handle) { 86 | nextEventInfoRef.current.handle = requestAnimationFrame( 87 | handlePointerMoveAnimation, 88 | ) 89 | } 90 | } 91 | 92 | throttledHandler.clear = clear 93 | 94 | return throttledHandler 95 | } 96 | -------------------------------------------------------------------------------- /src/useTimeout.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react' 2 | import useMounted from './useMounted.js' 3 | 4 | /* 5 | * Browsers including Internet Explorer, Chrome, Safari, and Firefox store the 6 | * delay as a 32-bit signed integer internally. This causes an integer overflow 7 | * when using delays larger than 2,147,483,647 ms (about 24.8 days), 8 | * resulting in the timeout being executed immediately. 9 | * 10 | * via: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout 11 | */ 12 | const MAX_DELAY_MS = 2 ** 31 - 1 13 | 14 | function setChainedTimeout( 15 | handleRef: MutableRefObject, 16 | fn: () => void, 17 | timeoutAtMs: number, 18 | ) { 19 | const delayMs = timeoutAtMs - Date.now() 20 | 21 | handleRef.current = 22 | delayMs <= MAX_DELAY_MS 23 | ? setTimeout(fn, delayMs) 24 | : setTimeout( 25 | () => setChainedTimeout(handleRef, fn, timeoutAtMs), 26 | MAX_DELAY_MS, 27 | ) 28 | } 29 | 30 | type TimeoutState = { 31 | fn: () => void 32 | delayMs: number 33 | } 34 | /** 35 | * Returns a controller object for setting a timeout that is properly cleaned up 36 | * once the component unmounts. New timeouts cancel and replace existing ones. 37 | * 38 | * ```tsx 39 | * const { set, clear } = useTimeout(); 40 | * const [hello, showHello] = useState(false); 41 | * //Display hello after 5 seconds 42 | * set(() => showHello(true), 5000); 43 | * return ( 44 | *
45 | * {hello ?

Hello

: null} 46 | *
47 | * ); 48 | * ``` 49 | */ 50 | export default function useTimeout() { 51 | const [timeout, setTimeoutState] = useState(null) 52 | const isMounted = useMounted() 53 | 54 | // types are confused between node and web here IDK 55 | const handleRef = useRef(null) 56 | 57 | useEffect(() => { 58 | if (!timeout) { 59 | return 60 | } 61 | 62 | const { fn, delayMs } = timeout 63 | 64 | function task() { 65 | if (isMounted()) { 66 | setTimeoutState(null) 67 | } 68 | fn() 69 | } 70 | 71 | if (delayMs <= MAX_DELAY_MS) { 72 | // For simplicity, if the timeout is short, just set a normal timeout. 73 | handleRef.current = setTimeout(task, delayMs) 74 | } else { 75 | setChainedTimeout(handleRef, task, Date.now() + delayMs) 76 | } 77 | const handle = handleRef.current 78 | 79 | return () => { 80 | // this should be a no-op since they are either the same or `handle` 81 | // already expired but no harm in calling twice 82 | if (handleRef.current !== handle) { 83 | clearTimeout(handle) 84 | } 85 | 86 | clearTimeout(handleRef.current) 87 | handleRef.current === null 88 | } 89 | }, [timeout]) 90 | 91 | const isPending = !!timeout 92 | 93 | return useMemo(() => { 94 | return { 95 | set(fn: () => void, delayMs = 0): void { 96 | if (!isMounted()) return 97 | 98 | setTimeoutState({ fn, delayMs }) 99 | }, 100 | clear() { 101 | setTimeoutState(null) 102 | }, 103 | isPending, 104 | handleRef, 105 | } 106 | }, [isPending, setTimeoutState, handleRef, isMounted]) 107 | } 108 | -------------------------------------------------------------------------------- /src/useToggleState.ts: -------------------------------------------------------------------------------- 1 | import { useReducer, Reducer } from 'react' 2 | 3 | /** 4 | * Create a state setter pair for a boolean value that can be "switched". 5 | * Unlike `useState(false)`, `useToggleState` will automatically flip the state 6 | * value when its setter is called with no argument. 7 | * 8 | * @param initialState The initial boolean value 9 | * @returns A tuple of the current state and a setter 10 | * 11 | * ```jsx 12 | * const [show, toggleShow] = useToggleState(false) 13 | * 14 | * return ( 15 | * <> 16 | *