├── .all-contributorsrc ├── .eslintrc ├── .gitignore ├── .prettierrc ├── .travis.yml ├── CONTRIBUTION.md ├── LICENSE ├── README-ARRAY.md ├── README.md ├── copyPackageJsonAndReadme.js ├── jest.config.js ├── package.json ├── src ├── array │ ├── index.test.ts │ ├── index.ts │ ├── useArray.ts │ ├── useBindToInput.ts │ ├── useBoolean.ts │ ├── useInput.ts │ ├── useMap.ts │ ├── useNumber.ts │ ├── useSet.ts │ └── useSetState.ts ├── index.test.ts ├── index.ts ├── useArray.ts ├── useBoolean.ts ├── useClickOutside.ts ├── useDelay.ts ├── useDocumentReady.ts ├── useFocus.ts ├── useGoogleAnalytics.ts ├── useImage.ts ├── useInput.ts ├── useLogger.ts ├── useMap.ts ├── useNumber.ts ├── useOnClick.ts ├── usePageLoad.ts ├── usePersist.ts ├── usePrevious.ts ├── useScript.ts ├── useSet.ts ├── useSetState.ts ├── useSizzyHooks.ts ├── useStateful.ts ├── useToggleBodyClass.ts └── useWindowSize.ts ├── tsconfig-require.json ├── tsconfig.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "RIP21", 10 | "name": "Andrey Los", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/3940079?v=4", 12 | "profile": "http://liveflow.io", 13 | "contributions": [ 14 | "ideas", 15 | "infra", 16 | "test", 17 | "code" 18 | ] 19 | }, 20 | { 21 | "login": "praneetrohida", 22 | "name": "Praneet Rohida", 23 | "avatar_url": "https://avatars.githubusercontent.com/u/23721710?v=4", 24 | "profile": "https://praneet.dev", 25 | "contributions": [ 26 | "infra", 27 | "test", 28 | "code" 29 | ] 30 | } 31 | ], 32 | "contributorsPerLine": 7, 33 | "projectName": "react-hanger", 34 | "projectOwner": "kitze", 35 | "repoType": "github", 36 | "repoHost": "https://github.com", 37 | "skipCi": true 38 | } 39 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "react-app", 4 | "prettier" 5 | ], 6 | "plugins": [ 7 | "prettier" 8 | ], 9 | "rules": { 10 | "import/no-named-default": 0, 11 | "prettier/prettier": "warn" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | .idea 8 | 9 | 10 | # builds 11 | build 12 | dist 13 | .rpt2_cache 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | !/src/array/index.test.ts 27 | /main/ 28 | /es6/ 29 | /lib/ 30 | tsconfig-require.tsbuildinfo 31 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 110, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | os: 5 | - windows 6 | - linux 7 | - osx 8 | cache: 9 | yarn: true 10 | install: 11 | - yarn install 12 | script: 13 | - yarn test 14 | - yarn build -------------------------------------------------------------------------------- /CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | # How to deploy 2 | 3 | `yarn release` 4 | 5 | This will do the trick, building, testing, publishing and redirecting you to GitHub to submit new release based on new tag 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kitze 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-ARRAY.md: -------------------------------------------------------------------------------- 1 | ## API Reference (array destructuring) 2 | 3 | ### How to import? 4 | 5 | ``` 6 | import { useBoolean } from 'react-hanger/array' // will import all of functions 7 | import useBoolean from 'react-hanger/array/useBoolean' // will import only this function 8 | ``` 9 | 10 | ### useBoolean 11 | 12 | ```jsx 13 | const [showCounter, actions] = useBoolean(true); 14 | ``` 15 | 16 | Actions: 17 | 18 | - `toggle` 19 | - `setTrue` 20 | - `setFalse` 21 | 22 | ### useNumber 23 | 24 | ```jsx 25 | const [counter] = useNumber(0); 26 | const [limitedNumber] = useNumber(3, { upperLimit: 5, lowerLimit: 3 }); 27 | const [rotatingNumber] = useNumber(0, { 28 | upperLimit: 5, 29 | lowerLimit: 0, 30 | loop: true 31 | }); 32 | ``` 33 | 34 | Actions: 35 | 36 | Both `increase` and `decrease` take an optional `amount` argument which is 1 by default, and will override the `step` property if it's used in the options. 37 | 38 | - `increase(amount = 1)` 39 | - `decrease(amount = 1 )` 40 | 41 | Options: 42 | 43 | - `lowerLimit` 44 | - `upperLimit` 45 | - `loop` 46 | - `step` - sets the increase/decrease amount of the number upfront, but it can still be overriden by `number.increase(3)` or `number.decrease(5)` 47 | 48 | ### useInput 49 | 50 | This one is unique, since it returns tuple as a first element, where first element is `value` and second is `hasValue` 51 | Second element is `actions` as usual 52 | 53 | ```typescript 54 | type UseInputActions = { 55 | setValue: React.Dispatch>; 56 | onChange: (e: React.SyntheticEvent) => void 57 | clear: () => void 58 | } 59 | type UseInput = [[string, boolean], UseInputActions] 60 | ``` 61 | 62 | ```jsx 63 | const [[newTodo], actions] = useInput(""); 64 | ``` 65 | 66 | ```jsx 67 | 68 | ``` 69 | UseSetActions: 70 | 71 | - `clear` 72 | - `onChange` - default native event.target.value handler 73 | 74 | Properties: 75 | 76 | - `hasValue` - 77 | 78 | ### useBindToInput 79 | 80 | Designed to be used in composition with `useInput`. 81 | First and second elements are the same as `useInput. 82 | Third are bindings to spread. 83 | 84 | ```jsx 85 | const [[newTodo], actions, { nativeBind, valueBind }] = useBindToInput(useInput("")); 86 | ``` 87 | 88 | ```jsx 89 | 90 | 91 | 92 | ``` 93 | 94 | UseSetActions: 95 | 96 | - `nativeBind` - binds the `value` and `onChange` props to an input that has `e.target.value` 97 | - `valueBind` - binds the `value` and `onChange` props to an input that's using only `value` in `onChange` (like most external components) 98 | 99 | ### useArray 100 | 101 | ```jsx 102 | const [todos, actions] = useArray([]); 103 | ``` 104 | 105 | UseSetActions: 106 | 107 | - `push` 108 | - `unshift` 109 | - `pop` 110 | - `shift` 111 | - `clear` 112 | - `removeIndex` 113 | - `removeById` - if array consists of objects with some specific `id` that you pass 114 | all of them will be removed 115 | - `modifyById` - if array consists of objects with some specific `id` that you pass 116 | these elements will be modified. 117 | - `move` - moves item from position to position shifting other elements. 118 | ``` 119 | So if input is [1, 2, 3, 4, 5] 120 | 121 | from | to | expected 122 | 3 | 0 | [4, 1, 2, 3, 5] 123 | -1 | 0 | [5, 1, 2, 3, 4] 124 | 1 | -2 | [1, 3, 4, 2, 5] 125 | -3 | -4 | [1, 3, 2, 4, 5] 126 | ``` 127 | 128 | ### useMap 129 | 130 | ```jsx 131 | const [someMap, someMapActions] = useMap([["key", "value"]]); 132 | const [anotherMap, anotherMapActions] = useMap(new Map([["key", "value"]])); 133 | ``` 134 | 135 | Actions: 136 | 137 | - `set` 138 | - `delete` 139 | - `clear` 140 | - `initialize` - applies tuples or map instances 141 | - `setValue` 142 | 143 | ### useSet 144 | 145 | ```jsx 146 | const [ value, actions ] = useSet(new Set([1, 2])) 147 | ``` 148 | 149 | `value` - a Set with only non mutating methods of a plain JS Set 150 | 151 | Actions: 152 | 153 | - `setValue` 154 | - `add` 155 | - `remove` 156 | - `clear` 157 | 158 | ## useSetState 159 | 160 | ```jsx 161 | const [state, setState, resetState] = useSetState({ loading: false }); 162 | setState({ loading: true, data: [1, 2, 3] }); 163 | ``` 164 | 165 | Actions: 166 | 167 | - `setState(value)` - will merge the `value` with the current `state` (like this.setState works in React) 168 | - `resetState()` - will reset the current `state` to the `value` which you pass to the `useSetState` hook 169 | 170 | Properties: 171 | 172 | - `state` - the current state 173 | 174 | ## usePrevious 175 | 176 | Use it to get the previous value of a prop or a state value. 177 | It's from the official [React Docs](https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state). 178 | It might come out of the box in the future. 179 | 180 | ```jsx 181 | const Counter = () => { 182 | const [count, setCount] = useState(0); 183 | const prevCount = usePrevious(count); 184 | return ( 185 |

186 | Now: {count}, before: {prevCount} 187 |

188 | ); 189 | }; 190 | ``` 191 | 192 | ### Migration from object to array based 193 | 194 | All value based hooks like `useBoolean`, `useNumber` etc. Are changed to 195 | be using arrays, since it's more safe for reference equality, and also 196 | makes it easier to use many `useSmth` without renaming `value` in destructuring. 197 | 198 | So if you had 199 | ```javascript 200 | const { value: showHeader, ...showHeaderActions } = useBoolean(true) 201 | const { value: showFooter, ...setShowFooterActions } = useBoolean(true) 202 | ``` 203 | It will become 204 | ```javascript 205 | const [showHeader, showHeaderActions] = useBoolean(true) 206 | const [showFooter, showFooterActions] = useBoolean(true) 207 | ``` 208 | 209 | Note that despite this code seems to be looking the same, it's not. Cause `showHeaderActions` in v1 will result 210 | in new object reference every rerender (because spreading creates new object, hence new reference). While in v2 actions are memoized 211 | using `useMemo` and their reference will not change, cause they are not rely on value. 212 | It enables us passing `actions` down the props without useless re-renders and excessive destructuring, it prevents `useEffects` and 213 | other hooks from re-run/new reference creation if autofix of ESLint rule `react-hooks/extraneous-deps` will add them as dependencies automatically. 214 | 215 | ### useInput migration 216 | Also big change to the `useInput` 217 | If before you was not using `eventBind` and `nativeBind` from them, then using the same approach from above 218 | you will get what you want. 219 | But if you need bindings you need to compose `useInput` with `useBindToInput` like that: 220 | So if you had 221 | ```jsx 222 | const { value, eventBind, valueBind, onChange, hasValue } = useInput("") 223 | 224 | 225 | 226 | {hasValue && } 227 | ``` 228 | It will become 229 | ```jsx 230 | const [[value, hasValue], actions, { eventBind, valueBind }] = useBindToInput(useInput("")) 231 | 232 | 233 | 234 | {hasValue && } 235 | ``` 236 | 237 | Note that first element in destructured array has tuple of `[value, hasValue]` since it's for values 238 | and second argument is for `actions` e.g. only for functions. 239 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 🙋‍♂️ Made by [@thekitze](https://twitter.com/thekitze) 2 | 3 | ### Other projects: 4 | - 💻 [Sizzy](https://sizzy.co) - A browser for designers and developers, focused on responsive design 5 | - 🏫 [React Academy](https://reactacademy.io) - Interactive React and GraphQL workshops 6 | - 🔮 [Glink](https://glink.so) - Changelogs, Roadmap, User Requests 7 | - 🐶 [Benji](https://benji.so) - Ultimate wellness and productivity platform 8 | - 🤖 [JSUI](https://github.com/kitze/JSUI) - A powerful UI toolkit for managing JavaScript apps 9 | - 📹 [YouTube Vlog](https://youtube.com/kitze) - Follow my journey 10 | 11 | Zero To Shipped 12 | 13 | # react-hanger 14 | 15 | [![npm version](https://badge.fury.io/js/react-hanger.svg)](https://badge.fury.io/js/react-hanger) 16 | 17 | [![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-) 18 | 19 | 20 | Set of a helpful hooks, for different specific to some primitives types state changing helpers. 21 | 22 | ## Contributors ✨ 23 | 24 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |

Andrey Los

🤔 🚇 ⚠️ 💻

Praneet Rohida

🚇 ⚠️ 💻
35 | 36 | 37 | 38 | 39 | 40 | 41 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 42 | 43 | --- 44 | 45 | Has two APIs: 46 | 47 | - [First](#Example) and original from v1 is based on object destructuring e.g. `const { value, toggle } = useBoolean(false)` (Docs below) 48 | - [Second API](./README-ARRAY.md) (recommended [why?](./README-ARRAY.md#migration-from-object-to-array-based)) is based on more idiomatic to React hooks API, e.g. like `useState` with array destructuring 49 | `const [value, actions] = useBoolean(false)` [(Docs)](./README-ARRAY.md) 50 | 51 | ## Install 52 | 53 | ```bash 54 | yarn add react-hanger 55 | ``` 56 | 57 | ## Usage 58 | 59 | ```jsx 60 | import React, { Component } from 'react'; 61 | 62 | import { useInput, useBoolean, useNumber, useArray, useOnMount, useOnUnmount } from 'react-hanger'; 63 | 64 | const App = () => { 65 | const newTodo = useInput(''); 66 | const showCounter = useBoolean(true); 67 | const limitedNumber = useNumber(3, { lowerLimit: 0, upperLimit: 5 }); 68 | const counter = useNumber(0); 69 | const todos = useArray(['hi there', 'sup', 'world']); 70 | 71 | const rotatingNumber = useNumber(0, { 72 | lowerLimit: 0, 73 | upperLimit: 4, 74 | loop: true, 75 | }); 76 | 77 | return ( 78 |
79 | 80 | 81 | {showCounter.value && {counter.value} } 82 | 83 | 84 | 85 |
86 | ); 87 | }; 88 | ``` 89 | 90 | ### Example 91 | 92 | [![Edit react-hanger example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/react-hanger-example-ize89?fontsize=14&hidenavigation=1&theme=dark) 93 | 94 | ## API reference (object destructuring) 95 | 96 | ### How to import? 97 | 98 | ``` 99 | import { useBoolean } from 'react-hanger' // will import all of functions 100 | import useBoolean from 'react-hanger/useBoolean' // will import only this function 101 | ``` 102 | 103 | ### useStateful 104 | 105 | Just an alternative syntax to `useState`, because it doesn't need array destructuring. 106 | It returns an object with `value` and a `setValue` method. 107 | 108 | ```jsx 109 | const username = useStateful('test'); 110 | 111 | username.setValue('tom'); 112 | console.log(username.value); 113 | ``` 114 | 115 | ### useBoolean 116 | 117 | ```jsx 118 | const showCounter = useBoolean(true); 119 | ``` 120 | 121 | Methods: 122 | 123 | - `toggle` 124 | - `setTrue` 125 | - `setFalse` 126 | 127 | ### useNumber 128 | 129 | ```jsx 130 | const counter = useNumber(0); 131 | const limitedNumber = useNumber(3, { upperLimit: 5, lowerLimit: 3 }); 132 | const rotatingNumber = useNumber(0, { 133 | upperLimit: 5, 134 | lowerLimit: 0, 135 | loop: true, 136 | }); 137 | ``` 138 | 139 | Methods: 140 | 141 | Both `increase` and `decrease` take an optional `amount` argument which is 1 by default, and will override the `step` property if it's used in the options. 142 | 143 | - `increase(amount = 1)` 144 | - `decrease(amount = 1 )` 145 | 146 | Options: 147 | 148 | - `lowerLimit` 149 | - `upperLimit` 150 | - `loop` 151 | - `step` - sets the increase/decrease amount of the number upfront, but it can still be overriden by `number.increase(3)` or `number.decrease(5)` 152 | 153 | ### useInput 154 | 155 | ```jsx 156 | const newTodo = useInput(''); 157 | ``` 158 | 159 | ```jsx 160 | 161 | ``` 162 | 163 | ```jsx 164 | 165 | 166 | ``` 167 | 168 | Methods: 169 | 170 | - `clear` 171 | - `onChange` 172 | - `eventBind` - binds the `value` and `onChange` props to an input that has `e.target.value` 173 | - `valueBind` - binds the `value` and `onChange` props to an input that's using only `value` in `onChange` (like most external components) 174 | 175 | Properties: 176 | 177 | - `hasValue` 178 | 179 | ### useArray 180 | 181 | ```jsx 182 | const todos = useArray([]); 183 | ``` 184 | 185 | Methods: 186 | 187 | - `push` - similar to native doesn't return length tho 188 | - `unshift` - similar to native doesn't return length tho 189 | - `pop` - similar to native doesn't return element tho 190 | - `shift` - similar to native doesn't return element tho 191 | - `clear` 192 | - `removeIndex` 193 | - `removeById` - if array consists of objects with some specific `id` that you pass 194 | all of them will be removed 195 | - `modifyById` - if array consists of objects with some specific `id` that you pass 196 | these elements will be modified. 197 | - `move` - moves item from position to position shifting other elements. 198 | 199 | ``` 200 | So if input is [1, 2, 3, 4, 5] 201 | 202 | from | to | expected 203 | 3 | 0 | [4, 1, 2, 3, 5] 204 | -1 | 0 | [5, 1, 2, 3, 4] 205 | 1 | -2 | [1, 3, 4, 2, 5] 206 | -3 | -4 | [1, 3, 2, 4, 5] 207 | ``` 208 | 209 | ### useMap 210 | 211 | ```jsx 212 | const { value, set } = useMap([['key', 'value']]); 213 | const { value: anotherValue, remove } = useMap(new Map([['key', 'value']])); 214 | ``` 215 | 216 | Actions: 217 | 218 | - `set` 219 | - `delete` 220 | - `clear` 221 | - `initialize` - applies tuples or map instances 222 | - `setValue` 223 | 224 | ### useSet 225 | 226 | ```jsx 227 | const set = useSet(new Set([1, 2])); 228 | ``` 229 | 230 | ### useSetState 231 | 232 | ```jsx 233 | const { state, setState, resetState } = useSetState({ loading: false }); 234 | setState({ loading: true, data: [1, 2, 3] }); 235 | resetState(); 236 | ``` 237 | 238 | Methods: 239 | 240 | - `setState(value)` - will merge the `value` with the current `state` (like this.setState works in React) 241 | - `resetState()` - will reset the current `state` to the `value` which you pass to the `useSetState` hook 242 | 243 | Properties: 244 | 245 | - `state` - the current state 246 | 247 | ### usePrevious 248 | 249 | Use it to get the previous value of a prop or a state value. 250 | It's from the official [React Docs](https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state). 251 | It might come out of the box in the future. 252 | 253 | ```jsx 254 | const Counter = () => { 255 | const [count, setCount] = useState(0); 256 | const prevCount = usePrevious(count); 257 | return ( 258 |

259 | Now: {count}, before: {prevCount} 260 |

261 | ); 262 | }; 263 | ``` 264 | 265 | ### usePageLoad 266 | 267 | ```jsx 268 | const isPageLoaded = usePageLoad(); 269 | ``` 270 | 271 | ### useScript 272 | 273 | ```jsx 274 | const { ready, error } = useScript({ 275 | src: 'https://example.com/script.js', 276 | startLoading: true, 277 | delay: 100, 278 | onReady: () => { 279 | console.log('Ready'); 280 | }, 281 | onError: (error) => { 282 | console.log('Error loading script ', error); 283 | }, 284 | }); 285 | ``` 286 | 287 | ### useDocumentReady 288 | 289 | ```jsx 290 | const isDocumentReady = useDocumentReady(); 291 | ``` 292 | 293 | ### useGoogleAnalytics 294 | 295 | ```jsx 296 | useGoogleAnalytics({ 297 | id: googleAnalyticsId, 298 | startLoading: true, 299 | delay: 500, 300 | }); 301 | ``` 302 | 303 | ### useWindowSize 304 | 305 | ```jsx 306 | const { width, height } = useWindowSize(); 307 | ``` 308 | 309 | ### useDelay 310 | 311 | ```jsx 312 | const done = useDelay(1000); 313 | ``` 314 | 315 | ### usePersist 316 | 317 | ```jsx 318 | const tokenValue = usePersist('auth-token', 'value'); 319 | ``` 320 | 321 | ### useToggleBodyClass 322 | 323 | ```jsx 324 | useToggleBodyClass(true, 'dark-mode'); 325 | ``` 326 | 327 | ### useOnClick 328 | 329 | ```jsx 330 | useOnClick((event) => { 331 | console.log('Click event fired: ', event); 332 | }); 333 | ``` 334 | 335 | ### useOnClickOutside 336 | 337 | ```jsx 338 | // Pass ref to the element 339 | const containerRef = useOnClickOutside(() => { 340 | console.log('Clicked outside container'); 341 | }); 342 | ``` 343 | 344 | ### useFocus 345 | 346 | ```jsx 347 | // pass ref to the element 348 | // call focusElement to focus the element 349 | const [elementRef, focusElement] = useFocus(); 350 | ``` 351 | 352 | ### useImage 353 | 354 | ```jsx 355 | const { imageVisible, bindToImage } = useImage(src, onLoad, onError); 356 | ``` 357 | 358 | ## Migration from v1 to v2 359 | 360 | - Migration to array based API is a bit more complex but recommended (especially if you're using ESLint rules for hooks). 361 | Take a look at [this section](./README-ARRAY.md#migration-from-object-to-array-based) in array API docs. 362 | - All lifecycle helpers are removed. Please replace `useOnMount`, `useOnUnmount` and `useLifecycleHooks` with `useEffect`. 363 | This: 364 | 365 | ```javascript 366 | useOnMount(() => console.log("I'm mounted!")); 367 | useOnUnmount(() => console.log("I'm unmounted")); 368 | // OR 369 | useLifecycleHooks({ 370 | onMount: () => console.log("I'm mounted!"), 371 | onUnmount: () => console.log("I'm unmounted!"), 372 | }); 373 | ``` 374 | 375 | to: 376 | 377 | ```javascript 378 | useEffect(() => { 379 | console.log("I'm mounted!"); 380 | return () => console.log("I'm unmounted"); 381 | }, []); 382 | ``` 383 | 384 | - `bind` and `bindToInput` are got renamed to `valueBind` and `eventBind` respectively on `useInput` hook 385 | -------------------------------------------------------------------------------- /copyPackageJsonAndReadme.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | fs.copyFileSync('package.json', './lib/package.json'); 4 | fs.copyFileSync('README.md', './lib/README.md'); 5 | fs.copyFileSync('README-ARRAY.md', './lib/README-ARRAY.md'); 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.(ts|tsx)$': 'ts-jest', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hanger", 3 | "version": "2.4.5", 4 | "description": "Set of a helpful hooks, for different specific to some primitives types state changing helpers", 5 | "author": "kitze", 6 | "license": "MIT", 7 | "repository": "kitze/react-hanger", 8 | "sideEffects": false, 9 | "contributors": [ 10 | { 11 | "name": "Andrii Los", 12 | "email": "puha212@gmail.com", 13 | "url": "https://github.com/RIP21" 14 | } 15 | ], 16 | "main": "./index.js", 17 | "module": "./esm/index.js", 18 | "typings": "./index.d.ts", 19 | "scripts": { 20 | "lint": "eslint ./src --ext ts,tsx --max-warnings 0", 21 | "lint:fix": "yarn lint --fix", 22 | "test": "jest src", 23 | "test:watch": "yarn test -- --watch", 24 | "build:esm": "tsc", 25 | "build:cjs": "tsc -p tsconfig-require.json", 26 | "clean": "rimraf lib", 27 | "build": "yarn build:esm && yarn build:cjs", 28 | "postbuild": "node copyPackageJsonAndReadme.js", 29 | "postversion": "yarn clean && yarn build", 30 | "release": "np --contents lib --no-yarn" 31 | }, 32 | "peerDependencies": { 33 | "react": ">16.8.0", 34 | "react-dom": ">16.8.0" 35 | }, 36 | "devDependencies": { 37 | "@testing-library/react": "^8.0.5", 38 | "@testing-library/react-hooks": "^1.1.0", 39 | "@types/jest": "^25.2.1", 40 | "@types/react": "^17.0.3", 41 | "@typescript-eslint/eslint-plugin": "4.22.0", 42 | "@typescript-eslint/parser": "4.22.0", 43 | "babel-eslint": "10.1.0", 44 | "eslint": "7.24.0", 45 | "eslint-config-prettier": "8.2.0", 46 | "eslint-config-react-app": "6.0.0", 47 | "eslint-plugin-flowtype": "5.7.1", 48 | "eslint-plugin-import": "2.22.1", 49 | "eslint-plugin-jsx-a11y": "6.4.1", 50 | "eslint-plugin-prettier": "3.4.0", 51 | "eslint-plugin-react": "7.23.2", 52 | "eslint-plugin-react-hooks": "4.2.0", 53 | "jest": "^25.3.0", 54 | "np": "6.2.1", 55 | "prettier": "2.2.1", 56 | "react": "^16.13.1", 57 | "react-dom": "^16.13.1", 58 | "react-test-renderer": "^16.13.1", 59 | "rimraf": "^3.0.2", 60 | "ts-jest": "^25.4.0", 61 | "typescript": "^4.2.4" 62 | }, 63 | "favoriteScripts": [ 64 | "test:watch" 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /src/array/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react-hooks'; 2 | import { cleanup } from '@testing-library/react'; 3 | import { useNumber } from './useNumber'; 4 | import { useArray } from './useArray'; 5 | import { useBoolean } from './useBoolean'; 6 | import { useInput } from './useInput'; 7 | import { useBindToInput } from './useBindToInput'; 8 | import { useSetState } from './useSetState'; 9 | import { useMap } from './useMap'; 10 | import { useSet } from './useSet'; 11 | 12 | afterEach(cleanup); 13 | describe('useNumber array', () => { 14 | it('should increase value with concrete value', () => { 15 | // given 16 | const { result } = renderHook(() => useNumber(5)); 17 | const [, actions] = result.current; 18 | // when 19 | act(() => actions.increase(5)); 20 | // then 21 | expect(result.current[0]).toBe(10); 22 | }); 23 | it('should increase value with concrete value with respect to upperLimit', () => { 24 | // given 25 | const { result } = renderHook(() => useNumber(5, { upperLimit: 10 })); 26 | const [, actions] = result.current; 27 | // when 28 | act(() => actions.increase(10)); 29 | // then 30 | expect(result.current[0]).toBe(10); 31 | }); 32 | it('should increase value by default step', () => { 33 | // given 34 | const { result } = renderHook(() => useNumber(5)); 35 | const [, actions] = result.current; 36 | // when 37 | act(() => actions.increase()); 38 | // then 39 | expect(result.current[0]).toBe(6); 40 | }); 41 | it('should increase value by default step with respect to upperLimit', () => { 42 | // given 43 | const { result } = renderHook(() => useNumber(5, { upperLimit: 5 })); 44 | const [, actions] = result.current; 45 | // when 46 | act(() => actions.increase()); 47 | // then 48 | expect(result.current[0]).toBe(5); 49 | }); 50 | it('should increase value by predefined step', () => { 51 | // given 52 | const { result } = renderHook(() => useNumber(5, { step: 3 })); 53 | const [, actions] = result.current; 54 | // when 55 | act(() => actions.increase()); 56 | // then 57 | expect(result.current[0]).toBe(8); 58 | }); 59 | it('should increase value by predefined step with respect to upperLimit', () => { 60 | // given 61 | const { result } = renderHook(() => useNumber(5, { step: 3, upperLimit: 7 })); 62 | const [, actions] = result.current; 63 | // when 64 | act(() => actions.increase()); 65 | // then 66 | expect(result.current[0]).toBe(7); 67 | }); 68 | it('should return lowerLimit value if nextValue is greater than upperLimit and loop', () => { 69 | // given 70 | const { result } = renderHook(() => 71 | useNumber(5, { 72 | upperLimit: 5, 73 | lowerLimit: 1, 74 | loop: true, 75 | }), 76 | ); 77 | const [, actions] = result.current; 78 | // when 79 | act(() => actions.increase()); 80 | // then 81 | expect(result.current[0]).toBe(1); 82 | }); 83 | it('should decrease value with concrete value', () => { 84 | // given 85 | const { result } = renderHook(() => useNumber(5)); 86 | const [, actions] = result.current; 87 | // when 88 | act(() => actions.decrease(5)); 89 | // then 90 | expect(result.current[0]).toBe(0); 91 | }); 92 | it('should decrease value with concrete value with respect to lowerLimit', () => { 93 | // given 94 | const { result } = renderHook(() => useNumber(5, { lowerLimit: 0 })); 95 | const [, actions] = result.current; 96 | // when 97 | act(() => actions.decrease(10)); 98 | // then 99 | expect(result.current[0]).toBe(0); 100 | }); 101 | it('should decrease value by default step', () => { 102 | // given 103 | const { result } = renderHook(() => useNumber(5)); 104 | const [, actions] = result.current; 105 | // when 106 | act(() => actions.decrease()); 107 | // then 108 | expect(result.current[0]).toBe(4); 109 | }); 110 | it('should decrease value by default step with respect to lowerLimit', () => { 111 | // given 112 | const { result } = renderHook(() => useNumber(5, { lowerLimit: 5 })); 113 | const [, actions] = result.current; 114 | // when 115 | act(() => actions.decrease()); 116 | // then 117 | expect(result.current[0]).toBe(5); 118 | }); 119 | it('should decrease value by predefined step', () => { 120 | // given 121 | const { result } = renderHook(() => useNumber(5, { step: 3 })); 122 | const [, actions] = result.current; 123 | // when 124 | act(() => actions.decrease()); 125 | // then 126 | expect(result.current[0]).toBe(2); 127 | }); 128 | it('should decrease value by predefined step with respect to lowerLimit', () => { 129 | // given 130 | const { result } = renderHook(() => useNumber(5, { step: 3, lowerLimit: 3 })); 131 | const [, actions] = result.current; 132 | // when 133 | act(() => actions.decrease()); 134 | // then 135 | expect(result.current[0]).toBe(3); 136 | }); 137 | 138 | describe('hooks optimizations', () => { 139 | it('should keep actions reference equality after value change', () => { 140 | // given 141 | const { result } = renderHook(() => useNumber(5)); 142 | const [, originalActionsReference] = result.current; 143 | expect(result.current[1]).toBe(originalActionsReference); 144 | // when 145 | act(() => originalActionsReference.increase(5)); 146 | // then 147 | expect(originalActionsReference).toBe(result.current[1]); 148 | }); 149 | }); 150 | }); 151 | 152 | describe('useInput array', () => { 153 | describe('hooks optimizations', () => { 154 | it('should keep actions reference equality after value change', () => { 155 | // given 156 | const { result } = renderHook(() => useInput(5)); 157 | const [, originalActionsReference] = result.current; 158 | expect(result.current[1]).toBe(originalActionsReference); 159 | // when 160 | act(() => originalActionsReference.setValue('1')); 161 | // then 162 | expect(originalActionsReference).toBe(result.current[1]); 163 | }); 164 | }); 165 | }); 166 | 167 | describe('useBindToInput', () => { 168 | describe('hooks optimizations', () => { 169 | it('should keep values array reference equality after rerender with no value change', () => { 170 | // given 171 | const { result, rerender } = renderHook(() => useBindToInput(useInput(5))); 172 | const [originalValueArray] = result.current; 173 | expect(result.current[0]).toBe(originalValueArray); 174 | // when 175 | act(() => rerender()); 176 | // then 177 | expect(originalValueArray).toEqual(result.current[0]); 178 | expect(originalValueArray).toBe(result.current[0]); 179 | }); 180 | it('should keep actions reference equality after value change', () => { 181 | // given 182 | const { result } = renderHook(() => useBindToInput(useInput(5))); 183 | const [, originalActionsReference] = result.current; 184 | expect(result.current[1]).toBe(originalActionsReference); 185 | // when 186 | act(() => originalActionsReference.setValue('1')); 187 | // then 188 | expect(originalActionsReference).toBe(result.current[1]); 189 | }); 190 | it('should not keep bindings reference equality after value change', () => { 191 | // given 192 | const { result } = renderHook(() => useBindToInput(useInput(5))); 193 | const [, actions, originalBindingsReference] = result.current; 194 | expect(result.current[2]).toBe(originalBindingsReference); 195 | // when 196 | act(() => actions.setValue('1')); 197 | // then 198 | expect(originalBindingsReference).not.toBe(result.current[2]); 199 | }); 200 | }); 201 | }); 202 | 203 | describe('useSetState array', () => { 204 | it('should change and merge state', () => { 205 | type State = { 206 | field: number; 207 | field2: number; 208 | field3?: number; 209 | }; 210 | const { result } = renderHook(() => useSetState({ field: 1, field2: 2 })); 211 | const [, setState] = result.current; 212 | 213 | expect(result.current[0]).toEqual({ field: 1, field2: 2 }); 214 | 215 | act(() => setState({ field: 2, field3: 3 })); 216 | 217 | expect(result.current[0]).toEqual({ field: 2, field2: 2, field3: 3 }); 218 | }); 219 | it('should reset state to initial value', () => { 220 | type State = { 221 | field: number; 222 | field2: number; 223 | field3?: number; 224 | }; 225 | const { result } = renderHook(() => useSetState({ field: 1, field2: 2 })); 226 | const [, setState, resetState] = result.current; 227 | 228 | expect(result.current[0]).toEqual({ field: 1, field2: 2 }); 229 | 230 | act(() => setState({ field: 2, field3: 3 })); 231 | 232 | expect(result.current[0]).toEqual({ field: 2, field2: 2, field3: 3 }); 233 | 234 | act(() => resetState()); 235 | 236 | expect(result.current[0]).toEqual({ field: 1, field2: 2 }); 237 | }); 238 | describe('hooks optimizations', () => { 239 | it('should keep actions reference equality after value change', () => { 240 | // given 241 | const { result } = renderHook(() => useSetState({})); 242 | const [, originalSetStateReference, originalResetStateReference] = result.current; 243 | expect(result.current[1]).toBe(originalSetStateReference); 244 | expect(result.current[2]).toBe(originalResetStateReference); 245 | // when 246 | act(() => originalSetStateReference({ field: 1 })); 247 | // then 248 | expect(originalSetStateReference).toBe(result.current[1]); 249 | 250 | // when 251 | act(() => originalResetStateReference()); 252 | // then 253 | expect(originalResetStateReference).toBe(result.current[2]); 254 | }); 255 | }); 256 | }); 257 | 258 | describe('useArray array', () => { 259 | it('should add item', () => { 260 | const { result } = renderHook(() => useArray([])); 261 | const [, actions] = result.current; 262 | expect(result.current[0].length).toBe(0); 263 | 264 | act(() => actions.push('test')); 265 | 266 | expect(result.current[0].length).toBe(1); 267 | }); 268 | 269 | it('should remove item by index', () => { 270 | const { result } = renderHook(() => useArray(['test', 'test1', 'test2'])); 271 | const [, actions] = result.current; 272 | expect(result.current[0].length).toBe(3); 273 | 274 | act(() => actions.removeIndex(1)); 275 | 276 | expect(result.current[0].length).toBe(2); 277 | expect(result.current[0][1]).toBe('test2'); 278 | }); 279 | 280 | it('should remove item by id', () => { 281 | const { result } = renderHook(() => useArray([{ id: 1 }, { id: 2 }])); 282 | const [, actions] = result.current; 283 | expect(result.current[0].length).toBe(2); 284 | 285 | act(() => actions.removeById(2)); 286 | 287 | expect(result.current[0].length).toBe(1); 288 | }); 289 | 290 | it('should modify item by id', () => { 291 | const { result } = renderHook(() => 292 | useArray([ 293 | { id: 1, foo: true }, 294 | { id: 2, foo: false }, 295 | ]), 296 | ); 297 | const [, actions] = result.current; 298 | expect(result.current[0].length).toBe(2); 299 | 300 | act(() => actions.modifyById(2, { foo: true })); 301 | 302 | const modifiedElement = result.current[0].find( 303 | (element: { id: number; foo: boolean }) => element.id === 2, 304 | ); 305 | 306 | expect(modifiedElement?.foo).toBe(true); 307 | }); 308 | 309 | it('should clear the array', () => { 310 | const { result } = renderHook(() => useArray([1, 2, 3, 4, 5])); 311 | const [, actions] = result.current; 312 | 313 | expect(result.current[0].length).toBe(5); 314 | 315 | act(() => actions.clear()); 316 | 317 | expect(result.current[0].length).toBe(0); 318 | }); 319 | 320 | it('should change array', () => { 321 | const { result } = renderHook(() => useArray([1, 2, 3, 4, 5])); 322 | const [, actions] = result.current; 323 | 324 | expect(result.current[0].length).toBe(5); 325 | 326 | act(() => actions.setValue((it) => [...it, 6])); 327 | 328 | expect(result.current[0].length).toBe(6); 329 | expect(result.current[0][5]).toBe(6); 330 | }); 331 | 332 | it.each` 333 | from | to | expected 334 | ${3} | ${0} | ${[4, 1, 2, 3, 5]} 335 | ${-1} | ${0} | ${[5, 1, 2, 3, 4]} 336 | ${1} | ${-2} | ${[1, 3, 4, 2, 5]} 337 | ${-3} | ${-4} | ${[1, 3, 2, 4, 5]} 338 | `('should move items in the array from: $from to: $to expected: $expected', ({ from, to, expected }) => { 339 | const { result } = renderHook(() => useArray([1, 2, 3, 4, 5])); 340 | const [, actions] = result.current; 341 | 342 | expect(result.current[0]).toEqual([1, 2, 3, 4, 5]); 343 | act(() => actions.move(from, to)); 344 | expect(result.current[0]).toEqual(expected); 345 | }); 346 | 347 | describe('hooks optimizations', () => { 348 | it('should keep actions reference equality after value change', () => { 349 | // given 350 | const { result } = renderHook(() => useArray([])); 351 | const [, originalActionsReference] = result.current; 352 | expect(result.current[1]).toBe(originalActionsReference); 353 | // when 354 | act(() => originalActionsReference.push(1)); 355 | // then 356 | expect(originalActionsReference).toBe(result.current[1]); 357 | }); 358 | }); 359 | }); 360 | 361 | describe('useBoolean array', () => { 362 | it('should set true', () => { 363 | const { result } = renderHook(() => useBoolean(false)); 364 | const [, actions] = result.current; 365 | 366 | expect(result.current[0]).toBe(false); 367 | 368 | act(() => actions.setTrue()); 369 | 370 | expect(result.current[0]).toBe(true); 371 | }); 372 | 373 | it('should set false', () => { 374 | const { result } = renderHook(() => useBoolean(true)); 375 | const [, actions] = result.current; 376 | 377 | expect(result.current[0]).toBe(true); 378 | 379 | act(() => actions.setFalse()); 380 | 381 | expect(result.current[0]).toBe(false); 382 | }); 383 | 384 | it('should toggle', () => { 385 | const { result } = renderHook(() => useBoolean(true)); 386 | const [, actions] = result.current; 387 | expect(result.current[0]).toBe(true); 388 | 389 | act(() => actions.toggle()); 390 | 391 | expect(result.current[0]).toBe(false); 392 | 393 | act(() => actions.toggle()); 394 | expect(result.current[0]).toBe(true); 395 | }); 396 | 397 | describe('hooks optimizations', () => { 398 | it('should keep actions reference equality after value change', () => { 399 | // given 400 | const { result } = renderHook(() => useBoolean(true)); 401 | const [, originalActionsReference] = result.current; 402 | expect(result.current[1]).toBe(originalActionsReference); 403 | // when 404 | act(() => originalActionsReference.setFalse()); 405 | // then 406 | expect(originalActionsReference).toBe(result.current[1]); 407 | }); 408 | }); 409 | }); 410 | 411 | describe('useSet array', () => { 412 | const initial = new Set([1, 2, 3]); 413 | 414 | it('should update old value', () => { 415 | // given 416 | const { result } = renderHook(() => useSet(initial)); 417 | const [value, { setValue }] = result.current; 418 | 419 | expect(value).toEqual(initial); 420 | // when 421 | act(() => setValue(new Set([2]))); 422 | // then 423 | expect(result.current[0]).toEqual(new Set([2])); 424 | }); 425 | 426 | it('should add new value', () => { 427 | // given 428 | const { result } = renderHook(() => useSet(initial)); 429 | const [, { add }] = result.current; 430 | // when 431 | act(() => add(4)); 432 | // then 433 | expect(result.current[0]).toEqual(new Set([1, 2, 3, 4])); 434 | }); 435 | 436 | it('should remove a value', () => { 437 | // given 438 | const { result } = renderHook(() => useSet(initial)); 439 | const [, { remove }] = result.current; 440 | // when 441 | act(() => remove(2)); 442 | // then 443 | expect(result.current[0]).toEqual(new Set([1, 3])); 444 | }); 445 | 446 | it('should clear', () => { 447 | // given 448 | const { result } = renderHook(() => useSet(initial)); 449 | const [, { clear }] = result.current; 450 | // when 451 | act(() => clear()); 452 | // then 453 | expect(result.current[0]).toEqual(new Set()); 454 | }); 455 | 456 | describe('hooks optimizations', () => { 457 | it('should change value reference equality after change', () => { 458 | // given 459 | const { result } = renderHook(() => useSet(initial)); 460 | const [originalValueReference, actions] = result.current; 461 | expect(result.current[0]).toBe(originalValueReference); 462 | // when 463 | act(() => actions.setValue(new Set([1]))); 464 | // then 465 | expect(originalValueReference).not.toBe(result.current[0]); 466 | }); 467 | 468 | it('should keep actions reference equality after value change', () => { 469 | // given 470 | const { result } = renderHook(() => useSet(initial)); 471 | const [, originalActionsReference] = result.current; 472 | expect(result.current[1]).toBe(originalActionsReference); 473 | // when 474 | act(() => originalActionsReference.setValue(new Set([1]))); 475 | // then 476 | expect(originalActionsReference).toBe(result.current[1]); 477 | }); 478 | }); 479 | }); 480 | 481 | describe('useMap array', () => { 482 | describe('set', () => { 483 | it('should update old value', () => { 484 | // given 485 | const { result } = renderHook(() => useMap([[1, 'default']])); 486 | const [, actions] = result.current; 487 | expect(result.current[0].get(1)).toBe('default'); 488 | // when 489 | act(() => actions.set(1, 'changed')); 490 | // then 491 | expect(result.current[0].get(1)).toBe('changed'); 492 | }); 493 | it('should add new value', () => { 494 | // given 495 | const { result } = renderHook(() => useMap()); 496 | const [, actions] = result.current; 497 | expect(result.current[0].get(1)).toBeUndefined(); 498 | // when 499 | act(() => actions.set(1, 'added')); 500 | // then 501 | expect(result.current[0].get(1)).toBe('added'); 502 | }); 503 | }); 504 | 505 | describe('delete', () => { 506 | it('should delete existing value', () => { 507 | // given 508 | const { result } = renderHook(() => useMap([[1, 'existing']])); 509 | const [, actions] = result.current; 510 | expect(result.current[0].get(1)).toBe('existing'); 511 | // when 512 | act(() => actions.delete(1)); 513 | // then 514 | expect(result.current[0].get(1)).toBeUndefined(); 515 | }); 516 | }); 517 | 518 | describe('initialize', () => { 519 | it.each` 520 | message | input 521 | ${'map'} | ${new Map([[1, 'initialized']])} 522 | ${'tuple'} | ${[[1, 'initialized']]} 523 | `('initializes with $message', ({ input }) => { 524 | // given 525 | const { result } = renderHook(() => useMap()); 526 | const [, actions] = result.current; 527 | expect(result.current[0].get(1)).toBeUndefined(); 528 | // when 529 | act(() => actions.initialize(input)); 530 | // then 531 | expect(result.current[0].get(1)).toBe('initialized'); 532 | }); 533 | }); 534 | 535 | describe('clear', () => { 536 | it('clears the map state and gets values', () => { 537 | // given 538 | const { result } = renderHook(() => useMap([[1, 'initialized']])); 539 | const [, actions] = result.current; 540 | expect(result.current[0].get(1)).toBe('initialized'); 541 | // when 542 | act(() => actions.clear()); 543 | // then 544 | expect(result.current[0].get(1)).toBeUndefined(); 545 | }); 546 | }); 547 | 548 | describe('hooks optimizations', () => { 549 | it('should change value reference equality after change', () => { 550 | // given 551 | const { result } = renderHook(() => useMap()); 552 | const [originalValueReference, actions] = result.current; 553 | expect(result.current[0]).toBe(originalValueReference); 554 | // when 555 | act(() => actions.set(1, 1)); 556 | // then 557 | expect(originalValueReference).not.toBe(result.current[0]); 558 | expect(originalValueReference.get(1)).toBeUndefined(); 559 | expect(result.current[0].get(1)).toBe(1); 560 | }); 561 | it('should keep actions reference equality after value change', () => { 562 | // given 563 | const { result } = renderHook(() => useMap()); 564 | const [, originalActionsReference] = result.current; 565 | expect(result.current[1]).toBe(originalActionsReference); 566 | // when 567 | act(() => originalActionsReference.set(1, 1)); 568 | // then 569 | expect(originalActionsReference).toBe(result.current[1]); 570 | }); 571 | }); 572 | }); 573 | -------------------------------------------------------------------------------- /src/array/index.ts: -------------------------------------------------------------------------------- 1 | export { useArray, UseArray, UseArrayActions } from './useArray'; 2 | export { useBindToInput, UseBindToInput, BindToInput } from './useBindToInput'; 3 | export { useBoolean, UseBoolean, UseBooleanActions } from './useBoolean'; 4 | export { useInput, UseInput, UseInputActions } from './useInput'; 5 | export { useMap, UseMap, MapOrEntries, UseMapActions, UseMapFunctions } from './useMap'; 6 | export { useNumber, UseNumber, UseNumberActions } from './useNumber'; 7 | export { useSet, UseSet, UseSetActions } from './useSet'; 8 | export { useSetState, UseSetState, UseSetStateAction } from './useSetState'; 9 | -------------------------------------------------------------------------------- /src/array/useArray.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useState } from 'react'; 2 | import { UseStateful } from '../useStateful'; 3 | 4 | export type UseArrayActions = { 5 | setValue: UseStateful['setValue']; 6 | add: (value: T | T[]) => void; 7 | push: (value: T | T[]) => void; 8 | pop: () => void; 9 | shift: () => void; 10 | unshift: (value: T | T[]) => void; 11 | clear: () => void; 12 | move: (from: number, to: number) => void; 13 | removeById: (id: T extends { id: string } ? string : T extends { id: number } ? number : unknown) => void; 14 | modifyById: ( 15 | id: T extends { id: string } ? string : T extends { id: number } ? number : unknown, 16 | newValue: Partial, 17 | ) => void; 18 | removeIndex: (index: number) => void; 19 | }; 20 | export type UseArray = [T[], UseArrayActions]; 21 | 22 | export function useArray(initial: T[]): UseArray { 23 | const [value, setValue] = useState(initial); 24 | const push = useCallback((a) => { 25 | setValue((v) => [...v, ...(Array.isArray(a) ? a : [a])]); 26 | }, []); 27 | const unshift = useCallback((a) => setValue((v) => [...(Array.isArray(a) ? a : [a]), ...v]), []); 28 | const pop = useCallback(() => setValue((v) => v.slice(0, -1)), []); 29 | const shift = useCallback(() => setValue((v) => v.slice(1)), []); 30 | const move = useCallback( 31 | (from: number, to: number) => 32 | setValue((it) => { 33 | const copy = it.slice(); 34 | copy.splice(to < 0 ? copy.length + to : to, 0, copy.splice(from, 1)[0]); 35 | return copy; 36 | }), 37 | [], 38 | ); 39 | const clear = useCallback(() => setValue(() => []), []); 40 | const removeById = useCallback( 41 | // @ts-ignore not every array that you will pass down will have object with id field. 42 | (id) => setValue((arr) => arr.filter((v) => v && v.id !== id)), 43 | [], 44 | ); 45 | const removeIndex = useCallback( 46 | (index) => 47 | setValue((v) => { 48 | const copy = v.slice(); 49 | copy.splice(index, 1); 50 | return copy; 51 | }), 52 | [], 53 | ); 54 | const modifyById = useCallback( 55 | (id, newValue) => 56 | // @ts-ignore not every array that you will pass down will have object with id field. 57 | setValue((arr) => arr.map((v) => (v.id === id ? { ...v, ...newValue } : v))), 58 | [], 59 | ); 60 | const actions = useMemo( 61 | () => ({ 62 | setValue, 63 | add: push, 64 | unshift, 65 | push, 66 | move, 67 | clear, 68 | removeById, 69 | removeIndex, 70 | pop, 71 | shift, 72 | modifyById, 73 | }), 74 | [modifyById, push, unshift, move, clear, removeById, removeIndex, pop, shift], 75 | ); 76 | return [value, actions]; 77 | } 78 | 79 | export default useArray; 80 | -------------------------------------------------------------------------------- /src/array/useBindToInput.ts: -------------------------------------------------------------------------------- 1 | import { default as React, useMemo } from 'react'; 2 | import { UseInput, UseInputActions } from './useInput'; 3 | 4 | export type BindToInput = { 5 | eventBind: { 6 | onChange: (e: React.SyntheticEvent) => void; 7 | value: string; 8 | }; 9 | valueBind: { 10 | onChange: React.Dispatch; 11 | value: string; 12 | }; 13 | }; 14 | export type UseBindToInput = [[string, boolean], UseInputActions, BindToInput]; 15 | 16 | export function useBindToInput(useInputResult: UseInput): UseBindToInput { 17 | const [values, actions] = useInputResult; 18 | return [ 19 | values, 20 | actions, 21 | useMemo( 22 | () => ({ 23 | eventBind: { 24 | onChange: actions.onChange, 25 | value: values[0], 26 | }, 27 | valueBind: { 28 | onChange: actions.setValue, 29 | value: values[0], 30 | }, 31 | }), 32 | [actions, values], 33 | ), 34 | ]; 35 | } 36 | 37 | export default useBindToInput; 38 | -------------------------------------------------------------------------------- /src/array/useBoolean.ts: -------------------------------------------------------------------------------- 1 | import { default as React, SetStateAction, useCallback, useMemo, useState } from 'react'; 2 | 3 | export type UseBooleanActions = { 4 | setValue: React.Dispatch>; 5 | toggle: () => void; 6 | setTrue: () => void; 7 | setFalse: () => void; 8 | }; 9 | export type UseBoolean = [boolean, UseBooleanActions]; 10 | 11 | export function useBoolean(initial: boolean): UseBoolean { 12 | const [value, setValue] = useState(initial); 13 | const toggle = useCallback(() => setValue((v) => !v), []); 14 | const setTrue = useCallback(() => setValue(true), []); 15 | const setFalse = useCallback(() => setValue(false), []); 16 | const actions = useMemo(() => ({ setValue, toggle, setTrue, setFalse }), [setFalse, setTrue, toggle]); 17 | return useMemo(() => [value, actions], [actions, value]); 18 | } 19 | 20 | export default useBoolean; 21 | -------------------------------------------------------------------------------- /src/array/useInput.ts: -------------------------------------------------------------------------------- 1 | import { default as React, SetStateAction, useCallback, useMemo, useState } from 'react'; 2 | 3 | export type UseInputActions = { 4 | setValue: React.Dispatch>; 5 | onChange: (e: React.BaseSyntheticEvent) => void; 6 | clear: () => void; 7 | }; 8 | export type UseInput = [[string, boolean], UseInputActions]; 9 | 10 | export function useInput(initial: string | number | boolean = ''): UseInput { 11 | const stringified = initial.toString(); 12 | const [value, setValue] = useState(stringified); 13 | const onChange = useCallback((e) => setValue(e.target.value.toString()), []); 14 | 15 | const clear = useCallback(() => setValue(''), []); 16 | const hasValue = value !== undefined && value !== null && value.trim() !== ''; 17 | const actions = useMemo( 18 | () => ({ 19 | setValue, 20 | clear, 21 | onChange, 22 | }), 23 | [clear, onChange], 24 | ); 25 | const values = useMemo(() => [value, hasValue], [hasValue, value]) as [string, boolean]; 26 | return [values, actions]; 27 | } 28 | 29 | export default useInput; 30 | -------------------------------------------------------------------------------- /src/array/useMap.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react'; 2 | 3 | export type MapOrEntries = Map | [K, V][]; 4 | export type UseMapActions = { 5 | setValue: Dispatch>>; 6 | remove: (keyToRemove: K) => void; 7 | delete: (keyToRemove: K) => void; 8 | set: (key: K, value: V) => void; 9 | clear: Map['clear']; 10 | initialize: (pairsOrMap: MapOrEntries) => void; 11 | }; 12 | 13 | export type UseMapFunctions = UseMapActions; // TODO: Remove on the next major release 14 | 15 | export type UseMap = [Map, UseMapActions]; 16 | 17 | export function useMap(initialState: MapOrEntries = new Map()): UseMap { 18 | const [map, setMap] = useState(Array.isArray(initialState) ? new Map(initialState) : initialState); 19 | 20 | const set = useCallback((key, value) => { 21 | setMap((aMap) => { 22 | const copy = new Map(aMap); 23 | return copy.set(key, value); 24 | }); 25 | }, []); 26 | 27 | const deleteByKey = useCallback((key) => { 28 | setMap((_map) => { 29 | const copy = new Map(_map); 30 | copy.delete(key); 31 | return copy; 32 | }); 33 | }, []); 34 | 35 | const clear = useCallback(() => { 36 | setMap(() => new Map()); 37 | }, []); 38 | 39 | const initialize = useCallback((mapOrTuple: MapOrEntries = []) => { 40 | setMap(() => new Map(mapOrTuple)); 41 | }, []); 42 | 43 | const actions = useMemo( 44 | () => ({ 45 | setValue: setMap, 46 | clear, 47 | set, 48 | // TODO: To be removed in the next major release 49 | remove: deleteByKey, 50 | delete: deleteByKey, 51 | initialize, 52 | }), 53 | [clear, deleteByKey, initialize, set], 54 | ); 55 | 56 | return [map, actions]; 57 | } 58 | 59 | export default useMap; 60 | -------------------------------------------------------------------------------- /src/array/useNumber.ts: -------------------------------------------------------------------------------- 1 | import { default as React, SetStateAction, useCallback, useMemo, useState } from 'react'; 2 | 3 | export type UseNumberActions = { 4 | setValue: React.Dispatch>; 5 | increase: (value?: number) => void; 6 | decrease: (value?: number) => void; 7 | }; 8 | export type UseNumber = [number, UseNumberActions]; 9 | 10 | export function useNumber( 11 | initial: number, 12 | { 13 | upperLimit, 14 | lowerLimit, 15 | loop, 16 | step = 1, 17 | }: { 18 | upperLimit?: number; 19 | lowerLimit?: number; 20 | loop?: boolean; 21 | step?: number; 22 | } = {}, 23 | ): UseNumber { 24 | const [value, setValue] = useState(initial); 25 | const decrease = useCallback( 26 | (d?: number) => { 27 | setValue((aValue) => { 28 | const decreaseBy = d !== undefined ? d : step; 29 | const nextValue = aValue - decreaseBy; 30 | 31 | if (lowerLimit !== undefined) { 32 | if (nextValue < lowerLimit) { 33 | if (loop && upperLimit) { 34 | return upperLimit; 35 | } 36 | 37 | return lowerLimit; 38 | } 39 | } 40 | 41 | return nextValue; 42 | }); 43 | }, 44 | [loop, lowerLimit, step, upperLimit], 45 | ); 46 | const increase = useCallback( 47 | (i?: number) => { 48 | setValue((aValue) => { 49 | const increaseBy = i !== undefined ? i : step; 50 | const nextValue = aValue + increaseBy; 51 | 52 | if (upperLimit !== undefined) { 53 | if (nextValue > upperLimit) { 54 | if (loop) { 55 | if (lowerLimit !== undefined) { 56 | return lowerLimit; 57 | } 58 | 59 | return initial; 60 | } 61 | 62 | return upperLimit; 63 | } 64 | } 65 | 66 | return nextValue; 67 | }); 68 | }, 69 | [step, upperLimit, loop, lowerLimit, initial], 70 | ); 71 | const actions = useMemo( 72 | () => ({ 73 | setValue, 74 | increase, 75 | decrease, 76 | }), 77 | [decrease, increase], 78 | ); 79 | return [value, actions]; 80 | } 81 | 82 | export default useNumber; 83 | -------------------------------------------------------------------------------- /src/array/useSet.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react'; 2 | 3 | export type UseSetActions = { 4 | setValue: Dispatch>>; 5 | add: (a: T) => void; 6 | remove: (a: T) => void; 7 | clear: Set['clear']; 8 | }; 9 | 10 | export type UseSet = [Set, UseSetActions]; 11 | 12 | const clone = (value: Set) => new Set(value); 13 | 14 | export function useSet(initialState: Set = new Set()): UseSet { 15 | const [value, setValue] = useState(initialState); 16 | const add = useCallback((item: T) => { 17 | setValue((prevValue) => { 18 | const copy = clone(prevValue); 19 | copy.add(item); 20 | return copy; 21 | }); 22 | }, []); 23 | 24 | const remove = useCallback((item: T) => { 25 | setValue((prevValue) => { 26 | const copy = clone(prevValue); 27 | copy.delete(item); 28 | return copy; 29 | }); 30 | }, []); 31 | 32 | const clear = useCallback(() => { 33 | setValue((prevValue) => { 34 | const copy = clone(prevValue); 35 | copy.clear(); 36 | return copy; 37 | }); 38 | }, []); 39 | 40 | const actions: UseSetActions = useMemo( 41 | () => ({ 42 | setValue, 43 | add, 44 | remove, 45 | clear, 46 | }), 47 | [add, clear, remove], 48 | ); 49 | 50 | return useMemo(() => [value, actions], [value, actions]); 51 | } 52 | -------------------------------------------------------------------------------- /src/array/useSetState.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { SetStateAction, useCallback, useState } from 'react'; 3 | 4 | export type UseSetStateAction = React.Dispatch>>; 5 | export type UseSetState = [T, UseSetStateAction, () => void]; 6 | 7 | export function useSetState(initialValue: T): UseSetState { 8 | const [value, setValue] = useState(initialValue); 9 | const setState = useCallback( 10 | (v: SetStateAction>) => { 11 | return setValue((oldValue) => ({ 12 | ...oldValue, 13 | ...(typeof v === 'function' ? v(oldValue) : v), 14 | })); 15 | }, 16 | [setValue], 17 | ); 18 | // Disabled on purpose to avoid new references on each render. 19 | // Since initialValue will be object and new reference is 20 | // guaranteed here, while values are the same, hence we can keep using old function 21 | // eslint-disable-next-line react-hooks/exhaustive-deps 22 | const resetState = useCallback(() => setValue(initialValue), []); 23 | 24 | return [value, setState, resetState]; 25 | } 26 | 27 | export default useSetState; 28 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react-hooks'; 2 | import { cleanup } from '@testing-library/react'; 3 | import { useStateful } from './useStateful'; 4 | import { useNumber } from './useNumber'; 5 | import { useArray } from './useArray'; 6 | import { useBoolean } from './useBoolean'; 7 | import { useInput } from './useInput'; 8 | import { useSetState } from './useSetState'; 9 | import { useMap } from './useMap'; 10 | import { useSet } from './useSet'; 11 | 12 | afterEach(cleanup); 13 | describe('useStateful', () => { 14 | it('should change value', () => { 15 | const { result } = renderHook(() => useStateful('initial')); 16 | expect(result.current.value).toBe('initial'); 17 | 18 | act(() => result.current.setValue('changed')); 19 | 20 | expect(result.current.value).toBe('changed'); 21 | }); 22 | }); 23 | 24 | describe('useNumber', () => { 25 | it('should increase value with concrete value', () => { 26 | // given 27 | const { result } = renderHook(() => useNumber(5)); 28 | const { increase } = result.current; 29 | // when 30 | act(() => increase(5)); 31 | // then 32 | expect(result.current.value).toBe(10); 33 | }); 34 | it('should increase value with concrete value with respect to upperLimit', () => { 35 | // given 36 | const { result } = renderHook(() => useNumber(5, { upperLimit: 10 })); 37 | const { increase } = result.current; 38 | // when 39 | act(() => increase(10)); 40 | // then 41 | expect(result.current.value).toBe(10); 42 | }); 43 | it('should increase value by default step', () => { 44 | // given 45 | const { result } = renderHook(() => useNumber(5)); 46 | const { increase } = result.current; 47 | // when 48 | act(() => increase()); 49 | // then 50 | expect(result.current.value).toBe(6); 51 | }); 52 | it('should increase value by default step with respect to upperLimit', () => { 53 | // given 54 | const { result } = renderHook(() => useNumber(5, { upperLimit: 5 })); 55 | const { increase } = result.current; 56 | // when 57 | act(() => increase()); 58 | // then 59 | expect(result.current.value).toBe(5); 60 | }); 61 | it('should increase value by predefined step', () => { 62 | // given 63 | const { result } = renderHook(() => useNumber(5, { step: 3 })); 64 | const { increase } = result.current; 65 | // when 66 | act(() => increase()); 67 | // then 68 | expect(result.current.value).toBe(8); 69 | }); 70 | it('should increase value by predefined step with respect to upperLimit', () => { 71 | // given 72 | const { result } = renderHook(() => useNumber(5, { step: 3, upperLimit: 7 })); 73 | const { increase } = result.current; 74 | // when 75 | act(() => increase()); 76 | // then 77 | expect(result.current.value).toBe(7); 78 | }); 79 | it('should decrease value with concrete value with', () => { 80 | // given 81 | const { result } = renderHook(() => useNumber(5)); 82 | const { decrease } = result.current; 83 | // when 84 | act(() => decrease(5)); 85 | // then 86 | expect(result.current.value).toBe(0); 87 | }); 88 | it('should decrease value with concrete value with respect to lowerLimit', () => { 89 | // given 90 | const { result } = renderHook(() => useNumber(5, { lowerLimit: 0 })); 91 | const { decrease } = result.current; 92 | // when 93 | act(() => decrease(10)); 94 | // then 95 | expect(result.current.value).toBe(0); 96 | }); 97 | it('should decrease value by default step', () => { 98 | // given 99 | const { result } = renderHook(() => useNumber(5)); 100 | const { decrease } = result.current; 101 | // when 102 | act(() => decrease()); 103 | // then 104 | expect(result.current.value).toBe(4); 105 | }); 106 | it('should decrease value by default step with respect to lowerLimit', () => { 107 | // given 108 | const { result } = renderHook(() => useNumber(5, { lowerLimit: 5 })); 109 | const { decrease } = result.current; 110 | // when 111 | act(() => decrease()); 112 | // then 113 | expect(result.current.value).toBe(5); 114 | }); 115 | it('should decrease value by predefined step', () => { 116 | // given 117 | const { result } = renderHook(() => useNumber(5, { step: 3 })); 118 | const { decrease } = result.current; 119 | // when 120 | act(() => decrease()); 121 | // then 122 | expect(result.current.value).toBe(2); 123 | }); 124 | it('should decrease value by predefined step with respect to lowerLimit', () => { 125 | // given 126 | const { result } = renderHook(() => useNumber(5, { step: 3, lowerLimit: 3 })); 127 | const { decrease } = result.current; 128 | // when 129 | act(() => decrease()); 130 | // then 131 | expect(result.current.value).toBe(3); 132 | }); 133 | 134 | describe('loop mode', () => { 135 | it('should go to lowerLimit value after increase reaching upperLimit', () => { 136 | // given 137 | const { result } = renderHook(() => useNumber(4, { loop: true, upperLimit: 5, lowerLimit: 0 })); 138 | const { increase } = result.current; 139 | // when 140 | act(() => increase()); 141 | act(() => increase()); 142 | // then 143 | expect(result.current.value).toBe(0); 144 | }); 145 | it('should go to initial value if no lowerLimit presented after increase reaching upperLimit', () => { 146 | // given 147 | const { result } = renderHook(() => useNumber(4, { loop: true, upperLimit: 5 })); 148 | const { increase } = result.current; 149 | // when 150 | act(() => increase()); 151 | act(() => increase()); 152 | // then 153 | expect(result.current.value).toBe(4); 154 | }); 155 | it('should stay on upperLimit after increase reaching its value if loop equals false', () => { 156 | // given 157 | const { result } = renderHook(() => useNumber(4, { loop: false, upperLimit: 5 })); 158 | const { increase } = result.current; 159 | // when 160 | act(() => increase()); 161 | act(() => increase()); 162 | // then 163 | expect(result.current.value).toBe(5); 164 | }); 165 | }); 166 | 167 | describe('hooks optimizations', () => { 168 | it('should keep actions reference equality after value change', () => { 169 | // given 170 | const { result } = renderHook(() => useNumber(5)); 171 | const { increase } = result.current; 172 | expect(result.current.increase).toBe(increase); 173 | // when 174 | act(() => increase(5)); 175 | // then 176 | expect(increase).toBe(result.current.increase); 177 | }); 178 | }); 179 | }); 180 | 181 | describe('useInput', () => { 182 | describe('hooks optimizations', () => { 183 | it('should keep actions reference equality after value change', () => { 184 | // given 185 | const { result } = renderHook(() => useInput(5)); 186 | const { setValue } = result.current; 187 | expect(result.current.setValue).toBe(setValue); 188 | // when 189 | act(() => setValue('1')); 190 | // then 191 | expect(setValue).toBe(result.current.setValue); 192 | }); 193 | }); 194 | }); 195 | 196 | describe('useSetState', () => { 197 | it('should change and merge state', () => { 198 | type State = { 199 | field: number; 200 | field2: number; 201 | field3?: number; 202 | }; 203 | const { result } = renderHook(() => useSetState({ field: 1, field2: 2 })); 204 | const { setState } = result.current; 205 | 206 | expect(result.current.state).toEqual({ field: 1, field2: 2 }); 207 | 208 | act(() => setState({ field: 2, field3: 3 })); 209 | 210 | expect(result.current.state).toEqual({ field: 2, field2: 2, field3: 3 }); 211 | }); 212 | it('should reset state to initial state', () => { 213 | type State = { 214 | field: number; 215 | field2: number; 216 | field3?: number; 217 | }; 218 | const { result } = renderHook(() => useSetState({ field: 1, field2: 2 })); 219 | const { setState, resetState } = result.current; 220 | 221 | expect(result.current.state).toEqual({ field: 1, field2: 2 }); 222 | 223 | act(() => setState({ field: 2, field3: 3 })); 224 | 225 | expect(result.current.state).toEqual({ field: 2, field2: 2, field3: 3 }); 226 | 227 | act(() => resetState()); 228 | 229 | expect(result.current.state).toEqual({ field: 1, field2: 2 }); 230 | }); 231 | describe('hooks optimizations', () => { 232 | it('should keep actions reference equality after value change', () => { 233 | // given 234 | const { result } = renderHook(() => useSetState<{}>({})); 235 | const { setState, resetState } = result.current; 236 | expect(result.current.setState).toBe(setState); 237 | // when 238 | act(() => setState([1])); 239 | // then 240 | expect(setState).toBe(result.current.setState); 241 | 242 | // when 243 | act(() => resetState()); 244 | // then 245 | expect(resetState).toBe(result.current.resetState); 246 | }); 247 | }); 248 | }); 249 | 250 | describe('useArray', () => { 251 | it('should push item', () => { 252 | const { result } = renderHook(() => useArray([])); 253 | const { push } = result.current; 254 | expect(result.current.value.length).toBe(0); 255 | 256 | act(() => { 257 | push('test'); 258 | }); 259 | 260 | expect(result.current.value.length).toBe(1); 261 | }); 262 | 263 | it('should remove item by index', () => { 264 | const { result } = renderHook(() => useArray(['test', 'test1', 'test2'])); 265 | const { removeIndex } = result.current; 266 | expect(result.current.value.length).toBe(3); 267 | 268 | act(() => removeIndex(1)); 269 | 270 | expect(result.current.value.length).toBe(2); 271 | expect(result.current.value[1]).toBe('test2'); 272 | }); 273 | 274 | it('should remove item by id', () => { 275 | const { result } = renderHook(() => useArray([{ id: 1 }, { id: 2 }])); 276 | const { removeById } = result.current; 277 | expect(result.current.value.length).toBe(2); 278 | 279 | act(() => removeById(2)); 280 | 281 | expect(result.current.value.length).toBe(1); 282 | }); 283 | 284 | it('should modify item by id', () => { 285 | const { result } = renderHook(() => 286 | useArray([ 287 | { id: 1, foo: true }, 288 | { id: 2, foo: false }, 289 | ]), 290 | ); 291 | const { modifyById } = result.current; 292 | expect(result.current.value.length).toBe(2); 293 | 294 | act(() => modifyById(2, { foo: true })); 295 | 296 | const modifiedElement = result.current.value.find( 297 | (element: { id: number; foo: boolean }) => element.id === 2, 298 | ); 299 | 300 | expect(modifiedElement?.foo).toBe(true); 301 | }); 302 | 303 | it('should clear the array', () => { 304 | const { result } = renderHook(() => useArray([1, 2, 3, 4, 5])); 305 | const { clear } = result.current; 306 | 307 | expect(result.current.value.length).toBe(5); 308 | 309 | act(() => clear()); 310 | 311 | expect(result.current.value.length).toBe(0); 312 | }); 313 | 314 | it('should change array', () => { 315 | const { result } = renderHook(() => useArray([1, 2, 3, 4, 5])); 316 | const { setValue } = result.current; 317 | 318 | expect(result.current.value.length).toBe(5); 319 | 320 | act(() => setValue((it) => [...it, 6])); 321 | 322 | expect(result.current.value.length).toBe(6); 323 | expect(result.current.value[5]).toBe(6); 324 | }); 325 | 326 | it.each` 327 | from | to | expected 328 | ${3} | ${0} | ${[4, 1, 2, 3, 5]} 329 | ${-1} | ${0} | ${[5, 1, 2, 3, 4]} 330 | ${1} | ${-2} | ${[1, 3, 4, 2, 5]} 331 | ${-3} | ${-4} | ${[1, 3, 2, 4, 5]} 332 | `('should move items in the array from: $from to: $to expected: $expected', ({ from, to, expected }) => { 333 | const { result } = renderHook(() => useArray([1, 2, 3, 4, 5])); 334 | const { move } = result.current; 335 | 336 | expect(result.current.value).toEqual([1, 2, 3, 4, 5]); 337 | act(() => move(from, to)); 338 | expect(result.current.value).toEqual(expected); 339 | }); 340 | 341 | describe('hooks optimizations', () => { 342 | it('should keep actions reference equality after value change', () => { 343 | // given 344 | const { result } = renderHook(() => useArray([])); 345 | const { push } = result.current; 346 | expect(result.current.push).toBe(push); 347 | // when 348 | act(() => { 349 | push(1); 350 | }); 351 | // then 352 | expect(push).toBe(result.current.push); 353 | }); 354 | }); 355 | }); 356 | 357 | describe('useBoolean', () => { 358 | it('should set true', () => { 359 | const { result } = renderHook(() => useBoolean(false)); 360 | const { setTrue } = result.current; 361 | 362 | expect(result.current.value).toBe(false); 363 | 364 | act(() => setTrue()); 365 | 366 | expect(result.current.value).toBe(true); 367 | }); 368 | 369 | it('should set false', () => { 370 | const { result } = renderHook(() => useBoolean(true)); 371 | const { setFalse } = result.current; 372 | 373 | expect(result.current.value).toBe(true); 374 | 375 | act(() => setFalse()); 376 | 377 | expect(result.current.value).toBe(false); 378 | }); 379 | 380 | it('should toggle', () => { 381 | const { result } = renderHook(() => useBoolean(true)); 382 | const { toggle } = result.current; 383 | expect(result.current.value).toBe(true); 384 | 385 | act(() => toggle()); 386 | 387 | expect(result.current.value).toBe(false); 388 | 389 | act(() => toggle()); 390 | expect(result.current.value).toBe(true); 391 | }); 392 | 393 | describe('hooks optimizations', () => { 394 | it('should keep actions reference equality after value change', () => { 395 | // given 396 | const { result } = renderHook(() => useBoolean(true)); 397 | const { setFalse } = result.current; 398 | expect(result.current.setFalse).toBe(setFalse); 399 | // when 400 | act(() => setFalse()); 401 | // then 402 | expect(setFalse).toBe(result.current.setFalse); 403 | }); 404 | }); 405 | }); 406 | 407 | describe('useSet', () => { 408 | const initial = new Set([1, 2, 3]); 409 | 410 | describe('hooks optimizations', () => { 411 | it('should change value reference equality after change', () => { 412 | // given 413 | const { result } = renderHook(() => useSet()); 414 | const value = result.current; 415 | // when 416 | act(() => value.setValue(initial)); 417 | // then 418 | expect(value).not.toBe(result.current); 419 | }); 420 | 421 | it('should keep actions reference equality after value change', () => { 422 | // given 423 | const { result } = renderHook(() => useSet()); 424 | const { setValue } = result.current; 425 | expect(result.current.setValue).toBe(setValue); 426 | // when 427 | act(() => setValue(new Set([1, 1]))); 428 | // then 429 | expect(setValue).toBe(result.current.setValue); 430 | }); 431 | }); 432 | }); 433 | 434 | describe('useMap', () => { 435 | describe('set', () => { 436 | it('should update old value', () => { 437 | // given 438 | const { result } = renderHook(() => useMap([[1, 'default']])); 439 | const { set } = result.current; 440 | expect(result.current.value.get(1)).toBe('default'); 441 | // when 442 | act(() => set(1, 'changed')); 443 | // then 444 | expect(result.current.value.get(1)).toBe('changed'); 445 | }); 446 | it('should add new value', () => { 447 | // given 448 | const { result } = renderHook(() => useMap()); 449 | const { set } = result.current; 450 | expect(result.current.value.get(1)).toBeUndefined(); 451 | // when 452 | act(() => set(1, 'added')); 453 | // then 454 | expect(result.current.value.get(1)).toBe('added'); 455 | }); 456 | }); 457 | 458 | describe('delete', () => { 459 | it('should delete existing value', () => { 460 | // given 461 | const { result } = renderHook(() => useMap([[1, 'existing']])); 462 | const { delete: aDelete } = result.current; 463 | expect(result.current.value.get(1)).toBe('existing'); 464 | // when 465 | act(() => aDelete(1)); 466 | // then 467 | expect(result.current.value.get(1)).toBeUndefined(); 468 | }); 469 | }); 470 | 471 | describe('initialize', () => { 472 | it.each` 473 | message | input 474 | ${'map'} | ${new Map([[1, 'initialized']])} 475 | ${'tuple'} | ${[[1, 'initialized']]} 476 | `('initializes with $message', ({ input }) => { 477 | // given 478 | const { result } = renderHook(() => useMap()); 479 | const { initialize } = result.current; 480 | expect(result.current.value.get(1)).toBeUndefined(); 481 | // when 482 | act(() => initialize(input)); 483 | // then 484 | expect(result.current.value.get(1)).toBe('initialized'); 485 | }); 486 | }); 487 | 488 | describe('clear', () => { 489 | it('clears the map state and gets values', () => { 490 | // given 491 | const { result } = renderHook(() => useMap([[1, 'initialized']])); 492 | const { clear } = result.current; 493 | expect(result.current.value.get(1)).toBe('initialized'); 494 | // when 495 | act(() => clear()); 496 | // then 497 | expect(result.current.value.get(1)).toBeUndefined(); 498 | }); 499 | }); 500 | 501 | describe('hooks optimizations', () => { 502 | it('should change value reference equality after change', () => { 503 | // given 504 | const { result } = renderHook(() => useMap()); 505 | const { value, set } = result.current; 506 | expect(result.current.value).toBe(value); 507 | // when 508 | act(() => set(1, 1)); 509 | // then 510 | expect(value).not.toBe(result.current.value); 511 | expect(value.get(1)).toBeUndefined(); 512 | expect(result.current.value.get(1)).toBe(1); 513 | }); 514 | it('should keep actions reference equality after value change', () => { 515 | // given 516 | const { result } = renderHook(() => useMap()); 517 | const { set } = result.current; 518 | expect(result.current.set).toBe(set); 519 | // when 520 | act(() => set(1, 1)); 521 | // then 522 | expect(set).toBe(result.current.set); 523 | }); 524 | }); 525 | }); 526 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { ClickOutsideOptions, useOnClickOutside } from './useClickOutside'; 2 | export { useArray, UseArray } from './useArray'; 3 | export { useBoolean, UseBoolean } from './useBoolean'; 4 | export { useDelay } from './useDelay'; 5 | export { useDocumentReady } from './useDocumentReady'; 6 | export { useFocus } from './useFocus'; 7 | export { useGoogleAnalytics, UseGoogleAnalyticsProps } from './useGoogleAnalytics'; 8 | export { useImage } from './useImage'; 9 | export { useInput, UseInput } from './useInput'; 10 | export { useLogger } from './useLogger'; 11 | export { useMap, UseMap, MapOrEntries } from './useMap'; 12 | export { useNumber, UseNumber } from './useNumber'; 13 | export { useOnClick } from './useOnClick'; 14 | export { usePageLoad } from './usePageLoad'; 15 | export { usePersist } from './usePersist'; 16 | export { usePrevious } from './usePrevious'; 17 | export { UseScript, useScript, UseScriptProps } from './useScript'; 18 | export { useSet, UseSet } from './useSet'; 19 | export { useSetState, UseSetState, UseSetStateAction } from './useSetState'; 20 | export { useStateful, UseStateful } from './useStateful'; 21 | export { useToggleBodyClass } from './useToggleBodyClass'; 22 | export { useWindowSize } from './useWindowSize'; 23 | -------------------------------------------------------------------------------- /src/useArray.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { UseStateful } from './useStateful'; 3 | import useArrayArray, { UseArrayActions } from './array/useArray'; 4 | 5 | export type UseArray = UseStateful & UseArrayActions; 6 | 7 | export function useArray(initial: T[]): UseArray { 8 | const [value, actions] = useArrayArray(initial); 9 | return useMemo( 10 | () => ({ 11 | value, 12 | ...actions, 13 | }), 14 | [actions, value], 15 | ); 16 | } 17 | 18 | export default useArray; 19 | -------------------------------------------------------------------------------- /src/useBoolean.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import useBooleanArray, { UseBooleanActions } from './array/useBoolean'; 3 | import { UseStateful } from './useStateful'; 4 | 5 | export type UseBoolean = UseStateful & UseBooleanActions; 6 | 7 | export function useBoolean(initial: boolean): UseBoolean { 8 | const [value, actions] = useBooleanArray(initial); 9 | return useMemo( 10 | () => ({ 11 | value, 12 | ...actions, 13 | }), 14 | [actions, value], 15 | ); 16 | } 17 | 18 | export default useBoolean; 19 | -------------------------------------------------------------------------------- /src/useClickOutside.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from 'react'; 2 | import { useOnClick } from './useOnClick'; 3 | 4 | export type ClickOutsideOptions = { 5 | blacklistClassNames?: string[]; 6 | blacklistElements?: string[]; 7 | }; 8 | const checkOptions = (event: MouseEvent, options?: ClickOutsideOptions) => { 9 | if (!options) { 10 | return true; 11 | } 12 | 13 | const { blacklistClassNames = [], blacklistElements = [] } = options; 14 | 15 | const passesClassnames = (event as any).path.every((element: HTMLElement) => { 16 | return blacklistClassNames.every((cn) => (element.classList ? !element.classList.contains(cn) : true)); 17 | }); 18 | 19 | const passesElements = blacklistElements.every((elem) => (event.target as HTMLElement)?.tagName !== elem); 20 | 21 | return passesClassnames && passesElements; 22 | }; 23 | 24 | export const useOnClickOutside = (fn: () => void, options?: ClickOutsideOptions) => { 25 | const elementRef = useRef(null); 26 | 27 | const handleClick = useCallback( 28 | (event: MouseEvent) => { 29 | if (!elementRef?.current?.contains(event.target as Node) && checkOptions(event, options)) { 30 | // clicked outside the ref 31 | fn(); 32 | } 33 | }, 34 | [fn, options], 35 | ); 36 | 37 | useOnClick(handleClick); 38 | 39 | return elementRef; 40 | }; 41 | -------------------------------------------------------------------------------- /src/useDelay.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export const useDelay = (delay: number = 0) => { 4 | const [done, setDone] = useState(false); 5 | useEffect(() => { 6 | setTimeout(() => { 7 | setDone(true); 8 | }, delay); 9 | }, [delay]); 10 | return done; 11 | }; 12 | -------------------------------------------------------------------------------- /src/useDocumentReady.ts: -------------------------------------------------------------------------------- 1 | import useBoolean from './useBoolean'; 2 | import { useEffect } from 'react'; 3 | 4 | export const useDocumentReady = () => { 5 | const { setTrue, value } = useBoolean(false); 6 | useEffect(() => { 7 | document.addEventListener('DOMContentLoaded', () => { 8 | setTrue(); 9 | }); 10 | }, [setTrue]); 11 | return value || (typeof document !== 'undefined' && document.readyState === 'complete'); 12 | }; 13 | -------------------------------------------------------------------------------- /src/useFocus.ts: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | 3 | export const useFocus = (): [React.RefObject, () => void] => { 4 | const ref = useRef(null); 5 | const focusElement = () => { 6 | requestAnimationFrame(() => { 7 | ref.current?.focus(); 8 | }); 9 | }; 10 | return [ref, focusElement]; 11 | }; 12 | -------------------------------------------------------------------------------- /src/useGoogleAnalytics.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export type UseGoogleAnalyticsProps = { 4 | id: string; 5 | startLoading: boolean; 6 | delay: number; 7 | }; 8 | 9 | export const useGoogleAnalytics = ({ id, startLoading, delay = 0 }: UseGoogleAnalyticsProps) => { 10 | useEffect(() => { 11 | if (startLoading) { 12 | if (!id) { 13 | throw new Error('Must provide id'); 14 | } 15 | setTimeout(() => { 16 | let script = document.createElement('script'); 17 | script.type = 'text/javascript'; 18 | script.src = `https://www.googletagmanager.com/gtag/js?id=${id}`; 19 | document.body.appendChild(script); 20 | //@ts-ignore 21 | window.dataLayer = window.dataLayer || []; 22 | 23 | function gtag() { 24 | //@ts-ignore 25 | window.dataLayer.push(arguments); 26 | } 27 | 28 | //@ts-ignore 29 | gtag('js', new Date()); 30 | //@ts-ignore 31 | gtag('config', id, { 32 | anonymize_ip: true, 33 | cookie_expires: 0, 34 | }); 35 | }, delay); 36 | } 37 | }, [delay, id, startLoading]); 38 | }; 39 | -------------------------------------------------------------------------------- /src/useImage.ts: -------------------------------------------------------------------------------- 1 | import { SyntheticEvent, useEffect } from 'react'; 2 | import useBoolean from './useBoolean'; 3 | 4 | export const useImage = ( 5 | src: string | undefined, 6 | onLoad?: (e: SyntheticEvent) => void, 7 | onError?: (e: string | SyntheticEvent) => void, 8 | ) => { 9 | const { setTrue, setFalse, value } = useBoolean(!!src); 10 | 11 | useEffect(() => { 12 | if (!src) { 13 | setFalse(); 14 | } 15 | }, [setFalse, src]); 16 | 17 | return { 18 | imageVisible: value, 19 | bindToImage: { 20 | hidden: !value, 21 | onLoad(e: SyntheticEvent) { 22 | setTrue(); 23 | onLoad && onLoad(e); 24 | }, 25 | onError(e: string | SyntheticEvent) { 26 | setTrue(); 27 | onError && onError(e); 28 | }, 29 | src, 30 | }, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/useInput.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useCallback, useMemo, useState } from 'react'; 3 | import { UseStateful } from './useStateful'; 4 | 5 | export type UseInput = UseStateful & { 6 | onChange: (e: React.BaseSyntheticEvent) => void; 7 | hasValue: boolean; 8 | clear: () => void; 9 | eventBind: { 10 | onChange: (e: React.BaseSyntheticEvent) => void; 11 | value: string; 12 | }; 13 | valueBind: { 14 | onChange: React.Dispatch; 15 | value: string; 16 | }; 17 | }; 18 | 19 | export function useInput(initial: string | number | boolean = ''): UseInput { 20 | const stringified = initial.toString(); 21 | const [value, setValue] = useState(stringified); 22 | const onChange = useCallback((e) => setValue(e.target.value), []); 23 | 24 | const clear = useCallback(() => setValue(''), []); 25 | return useMemo( 26 | () => ({ 27 | value, 28 | setValue, 29 | hasValue: value !== undefined && value !== null && value.trim() !== '', 30 | clear, 31 | onChange, 32 | eventBind: { 33 | onChange, 34 | value, 35 | }, 36 | valueBind: { 37 | onChange: setValue, 38 | value, 39 | }, 40 | }), 41 | [clear, onChange, value], 42 | ); 43 | } 44 | 45 | export default useInput; 46 | -------------------------------------------------------------------------------- /src/useLogger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { useEffect } from 'react'; 3 | 4 | export function useLogger(name: string, props: any): void { 5 | useEffect(() => { 6 | console.log(`${name} has mounted`); 7 | return () => console.log(`${name} has unmounted`); 8 | }, [name]); 9 | useEffect(() => { 10 | console.log(`${name} Props updated`, props); 11 | }); 12 | } 13 | 14 | export default useLogger; 15 | -------------------------------------------------------------------------------- /src/useMap.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { UseStateful } from './useStateful'; 3 | import useMapArray, { UseMapActions } from './array/useMap'; 4 | 5 | export type MapOrEntries = Map | [K, V][]; 6 | export type UseMap = UseStateful> & UseMapActions; 7 | 8 | export function useMap(initialState: MapOrEntries = new Map()): UseMap { 9 | const [map, actions] = useMapArray(initialState); 10 | return useMemo( 11 | () => ({ 12 | value: map, 13 | ...actions, 14 | }), 15 | [actions, map], 16 | ); 17 | } 18 | 19 | export default useMap; 20 | -------------------------------------------------------------------------------- /src/useNumber.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { UseStateful } from './useStateful'; 3 | import useNumberArray, { UseNumberActions } from './array/useNumber'; 4 | 5 | export type UseNumber = UseStateful & UseNumberActions; 6 | 7 | export function useNumber( 8 | initial: number, 9 | options: { 10 | upperLimit?: number; 11 | lowerLimit?: number; 12 | loop?: boolean; 13 | step?: number; 14 | } = {}, 15 | ): UseNumber { 16 | const [value, actions] = useNumberArray(initial, options); 17 | return useMemo( 18 | () => ({ 19 | value, 20 | ...actions, 21 | }), 22 | [actions, value], 23 | ); 24 | } 25 | 26 | export default useNumber; 27 | -------------------------------------------------------------------------------- /src/useOnClick.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export const useOnClick = (handler: (event: MouseEvent) => void) => { 4 | useEffect(() => { 5 | document.addEventListener('mousedown', handler); 6 | return () => document.removeEventListener('mousedown', handler); 7 | }, [handler]); 8 | }; 9 | -------------------------------------------------------------------------------- /src/usePageLoad.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import useBoolean from './useBoolean'; 3 | 4 | export const usePageLoad = () => { 5 | const { value, setTrue } = useBoolean(false); 6 | useEffect(() => { 7 | window.onload = () => setTrue(); 8 | }, [setTrue]); 9 | return value; 10 | }; 11 | -------------------------------------------------------------------------------- /src/usePersist.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export const usePersist = (key: string, data: T): T => { 4 | const storageKey = `persist-cache-${key}`; 5 | const storageValue = localStorage.getItem(storageKey); 6 | const persistedValue = JSON.parse(storageValue ?? '{}'); 7 | const [state, setState] = useState(storageValue ? persistedValue : data); 8 | 9 | useEffect(() => { 10 | if (data) { 11 | setState(data); 12 | localStorage.setItem(storageKey, JSON.stringify(data)); 13 | } 14 | }, [data, storageKey]); 15 | return state; 16 | }; 17 | -------------------------------------------------------------------------------- /src/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export function usePrevious(value: T): T | undefined { 4 | const ref = useRef(); 5 | useEffect(() => { 6 | ref.current = value; 7 | }, [value]); 8 | return ref.current; 9 | } 10 | 11 | export default usePrevious; 12 | -------------------------------------------------------------------------------- /src/useScript.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | 3 | export type UseScriptProps = { 4 | delay?: number; 5 | src: string; 6 | onError?: (error: Event | string) => void; 7 | onReady?: () => void; 8 | startLoading: boolean; 9 | }; 10 | 11 | export type UseScript = { 12 | ready: boolean; 13 | error: null | Event | string; 14 | }; 15 | 16 | export const useScript = ({ startLoading, onReady, onError, src, delay = 0 }: UseScriptProps): UseScript => { 17 | const isLoading = useRef(false); 18 | 19 | const [state, setState] = useState({ 20 | ready: false, 21 | error: null, 22 | }); 23 | 24 | useEffect(() => { 25 | if (startLoading && !isLoading.current) { 26 | setTimeout(() => { 27 | const script = document.createElement('script'); 28 | script.src = src; 29 | script.onload = () => { 30 | setState({ ready: true, error: null }); 31 | onReady?.(); 32 | }; 33 | script.onerror = (error) => { 34 | setState({ ready: false, error }); 35 | onError?.(error); 36 | }; 37 | document.body.appendChild(script); 38 | isLoading.current = true; 39 | }, delay); 40 | } 41 | }, [startLoading, delay, src, onReady, onError]); 42 | 43 | return state; 44 | }; 45 | -------------------------------------------------------------------------------- /src/useSet.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useSet as useSetArray, UseSetActions } from './array/useSet'; 3 | import type { UseStateful } from './useStateful'; 4 | 5 | export interface UseSet extends UseStateful>, UseSetActions {} 6 | 7 | export function useSet(initialState: Set = new Set()): UseSet { 8 | const [value, actions] = useSetArray(initialState); 9 | 10 | return useMemo(() => { 11 | return { 12 | value, 13 | ...actions, 14 | }; 15 | }, [actions, value]); 16 | } 17 | -------------------------------------------------------------------------------- /src/useSetState.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { SetStateAction, useMemo } from 'react'; 3 | import useSetStateArray from './array/useSetState'; 4 | 5 | export type UseSetStateAction = React.Dispatch>>; 6 | export type UseSetState = { 7 | setState: UseSetStateAction; 8 | state: T; 9 | resetState: () => void; 10 | }; 11 | 12 | export function useSetState(initialValue: T): UseSetState { 13 | const [state, setState, resetState] = useSetStateArray(initialValue); 14 | return useMemo( 15 | () => ({ 16 | setState, 17 | resetState, 18 | state, 19 | }), 20 | [setState, resetState, state], 21 | ); 22 | } 23 | 24 | export default useSetState; 25 | -------------------------------------------------------------------------------- /src/useSizzyHooks.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback, RefObject } from 'react'; 2 | 3 | export const useCanHover = () => { 4 | // assume that if device is smaller than 500 there's no hover, but actually check it on the first touch event 5 | const [canHover, setCanHover] = useState(window.innerWidth > 500); 6 | useEffect(() => { 7 | // mobile devices also emit a "mousemove" on every touch (#theplatform<3), but desktop devices don't emit "touchstart" 8 | const eventName = 'touchstart'; 9 | window.addEventListener( 10 | eventName, 11 | function onFirstTouch() { 12 | setCanHover(false); 13 | window.removeEventListener(eventName, onFirstTouch, false); 14 | }, 15 | false, 16 | ); 17 | }, []); 18 | return canHover; 19 | }; 20 | 21 | export const useHovered = () => { 22 | const [hovering, setHovering] = useState(false); 23 | const canHover = useCanHover(); 24 | 25 | return { 26 | value: hovering, 27 | setValue: setHovering, 28 | bind: canHover 29 | ? { 30 | onMouseOver: () => setHovering(true), 31 | onMouseLeave: () => setHovering(false), 32 | } 33 | : { 34 | onClick: () => setHovering((h) => !h), 35 | }, 36 | }; 37 | }; 38 | 39 | export const usePose = (initial: string, poses: object = {}) => { 40 | const [pose, setPose] = useState(initial); 41 | return { pose, setPose, poses }; 42 | }; 43 | 44 | export const useVisiblePose = (initial: any) => { 45 | const VISIBLE = 'visible'; 46 | const HIDDEN = 'hidden'; 47 | const { setPose, pose, ...rest } = usePose(initial ? VISIBLE : HIDDEN, [HIDDEN, VISIBLE]); 48 | return [pose, (v: any) => setPose(v ? VISIBLE : HIDDEN), rest]; 49 | }; 50 | 51 | export const useFindElementCenter = (elementRef: RefObject) => { 52 | const [windowSize, setWindowSize] = useState<{ x: number; y: number }>(); 53 | useEffect(() => { 54 | if (elementRef.current) { 55 | const { offsetTop, offsetLeft, offsetWidth, offsetHeight } = elementRef.current; 56 | setWindowSize({ 57 | x: window.innerWidth / 2 - offsetWidth / 2 - offsetLeft, 58 | y: window.innerHeight / 2 - offsetHeight / 2 - offsetTop, 59 | }); 60 | } 61 | }, [elementRef]); 62 | return windowSize; 63 | }; 64 | 65 | export const useMousePosition = (shouldTrack: boolean) => { 66 | const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); 67 | const canHover = useCanHover(); 68 | 69 | useEffect(() => { 70 | if (canHover && shouldTrack) { 71 | const handler = ({ clientX, clientY }: { clientX: number; clientY: number }) => { 72 | setMousePosition({ 73 | x: clientX, 74 | y: clientY, 75 | }); 76 | }; 77 | window.document.addEventListener('mousemove', handler); 78 | return () => window.document.removeEventListener('mousemove', handler); 79 | } 80 | return () => {}; 81 | }, [canHover, shouldTrack]); 82 | 83 | return canHover ? mousePosition : {}; 84 | }; 85 | 86 | export type TimeFormattingFunction = (date: Date) => string; 87 | 88 | export const useClock = ( 89 | timeFormattingFunction: TimeFormattingFunction = (date) => date.toLocaleDateString(), 90 | ) => { 91 | const getCurrentTime = useCallback(() => timeFormattingFunction(new Date()), [timeFormattingFunction]); 92 | const [time, setTime] = useState(getCurrentTime()); 93 | 94 | useEffect(() => { 95 | const t = setInterval(() => setTime(getCurrentTime()), 1000); 96 | 97 | return () => clearInterval(t); 98 | }, [getCurrentTime]); 99 | 100 | return time; 101 | }; 102 | -------------------------------------------------------------------------------- /src/useStateful.ts: -------------------------------------------------------------------------------- 1 | import { default as React, SetStateAction, useMemo, useState } from 'react'; 2 | 3 | export function useStateful(initial: T): UseStateful { 4 | const [value, setValue] = useState(initial); 5 | return useMemo( 6 | () => ({ 7 | value, 8 | setValue, 9 | }), 10 | [value], 11 | ); 12 | } 13 | 14 | export type UseStateful = { 15 | value: T; 16 | setValue: React.Dispatch>; 17 | }; 18 | 19 | export default useStateful; 20 | -------------------------------------------------------------------------------- /src/useToggleBodyClass.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export const useToggleBodyClass = (addClass: boolean, className: string) => { 4 | useEffect(() => { 5 | addClass ? document.body.classList.add(className) : document.body.classList.remove(className); 6 | }, [addClass, className]); 7 | }; 8 | -------------------------------------------------------------------------------- /src/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export const useWindowSize = () => { 4 | const [size, setSize] = useState({ width: 0, height: 0 }); 5 | 6 | useEffect(() => { 7 | let measure = () => { 8 | setSize({ 9 | width: window.innerWidth, 10 | height: window.innerHeight, 11 | }); 12 | }; 13 | measure(); 14 | window.addEventListener('resize', measure); 15 | return () => window.removeEventListener('resize', measure); 16 | }, []); 17 | 18 | return size; 19 | }; 20 | -------------------------------------------------------------------------------- /tsconfig-require.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es2017", 6 | "outDir": "./lib" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src/", 4 | "rootDir": "src", 5 | "downlevelIteration": true, 6 | "lib": ["dom", "dom.iterable", "esnext"], 7 | "module": "es6", 8 | "target": "es6", 9 | "esModuleInterop": true, 10 | "jsx": "preserve", 11 | "experimentalDecorators": true, 12 | "moduleResolution": "node", 13 | "forceConsistentCasingInFileNames": false, 14 | "noUnusedParameters": true, 15 | "noUnusedLocals": true, 16 | "suppressImplicitAnyIndexErrors": true, 17 | "skipLibCheck": true, 18 | "resolveJsonModule": true, 19 | "strict": true, 20 | "allowSyntheticDefaultImports": true, 21 | "emitDecoratorMetadata": true, 22 | "outDir": "./lib/esm", 23 | "declaration": true, 24 | "sourceMap": true 25 | }, 26 | "exclude": [ 27 | "lib", 28 | "src/**/*.js", 29 | "node_modules", 30 | "example", 31 | "jest", 32 | "src/setupTests.ts", 33 | "src/index.test.ts", 34 | "src/array/index.test.ts" 35 | ], 36 | "include": [ 37 | "src" 38 | ] 39 | } 40 | --------------------------------------------------------------------------------