├── rollup.config.js ├── tsconfig.json ├── src ├── index.js └── index.d.ts ├── package.json ├── LICENSE ├── .gitignore ├── yarn.lock └── README.md /rollup.config.js: -------------------------------------------------------------------------------- 1 | export default ["umd", "esm"].map((format) => ({ 2 | input: "src/index.js", 3 | output: { 4 | file: `dist/index.${format}.js`, 5 | format, 6 | name: "useRefs", 7 | globals: { 8 | react: "React", 9 | }, 10 | }, 11 | external: ["react"], 12 | })); 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | } 21 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | 3 | const MAX_ITERATIONS_COUNT = 50; 4 | 5 | function iterator() { 6 | return this; 7 | } 8 | 9 | export default function useMultipleRefs(initialValue) { 10 | let count = 0; 11 | 12 | return { 13 | next() { 14 | if (++count > MAX_ITERATIONS_COUNT) { 15 | throw new Error( 16 | "useMultipleRefs: reached more than 50 refs. This hook can be used exclusively with the array destructuring syntax." 17 | ); 18 | } 19 | 20 | return { 21 | done: false, 22 | value: useRef(initialValue), 23 | }; 24 | }, 25 | [Symbol.iterator]: iterator, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-use-refs", 3 | "description": "Hook to create multiple refs in a single call", 4 | "version": "1.0.1", 5 | "main": "dist/index.umd.js", 6 | "module": "dist/index.esm.js", 7 | "typings": "src/index.d.ts", 8 | "author": "Pier Paolo Ramon ", 9 | "license": "MIT", 10 | "files": [ 11 | "package.json", 12 | "dist", 13 | "src" 14 | ], 15 | "scripts": { 16 | "build": "rollup --config" 17 | }, 18 | "peerDependencies": { 19 | "react": ">=16.8.0" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^17.0.32", 23 | "react": "^17.0.2", 24 | "rollup": "^2.58.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type Tuple = [T] | T[]; 4 | type IsTuple = T extends any[] ? (any[] extends T ? never : T) : never; 5 | type RefObjectOf = null extends T 6 | ? React.RefObject 7 | : React.MutableRefObject; 8 | 9 | export default function useRefs( 10 | initialValue: T extends IsTuple ? null : never 11 | ): T extends IsTuple ? { [I in keyof T]: RefObjectOf } : never; 12 | 13 | // function useRef(initialValue: T): MutableRefObject; 14 | 15 | export default function useRefs( 16 | initialValue: T 17 | ): React.MutableRefObject[]; 18 | 19 | // function useRef(initialValue: T|null): RefObject; 20 | 21 | export default function useRefs( 22 | initialValue: T | null 23 | ): React.RefObject[]; 24 | 25 | // function useRef(): MutableRefObject; 26 | 27 | export default function useRefs(): React.MutableRefObject< 28 | T | undefined 29 | >[]; 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 Pier Paolo Ramon 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/prop-types@*": 6 | version "15.7.4" 7 | resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" 8 | integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== 9 | 10 | "@types/react@^17.0.32": 11 | version "17.0.32" 12 | resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.32.tgz#89a161286bbe2325d4d516420a27364a324909f4" 13 | integrity sha512-hAm1pmwA3oZWbkB985RFwNvBRMG0F3KWSiC4/hNmanigKZMiKQoH5Q6etNw8HIDztTGfvXyOjPvdNnvBUCuaPg== 14 | dependencies: 15 | "@types/prop-types" "*" 16 | "@types/scheduler" "*" 17 | csstype "^3.0.2" 18 | 19 | "@types/scheduler@*": 20 | version "0.16.2" 21 | resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" 22 | integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== 23 | 24 | csstype@^3.0.2: 25 | version "3.0.9" 26 | resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.9.tgz#6410af31b26bd0520933d02cbc64fce9ce3fbf0b" 27 | integrity sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw== 28 | 29 | fsevents@~2.3.2: 30 | version "2.3.2" 31 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 32 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 33 | 34 | "js-tokens@^3.0.0 || ^4.0.0": 35 | version "4.0.0" 36 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 37 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 38 | 39 | loose-envify@^1.1.0: 40 | version "1.4.0" 41 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" 42 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== 43 | dependencies: 44 | js-tokens "^3.0.0 || ^4.0.0" 45 | 46 | object-assign@^4.1.1: 47 | version "4.1.1" 48 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 49 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 50 | 51 | react@^17.0.2: 52 | version "17.0.2" 53 | resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" 54 | integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== 55 | dependencies: 56 | loose-envify "^1.1.0" 57 | object-assign "^4.1.1" 58 | 59 | rollup@^2.58.1: 60 | version "2.58.1" 61 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.58.1.tgz#b9a0f9e601129d9a4686b7ec3159095f7a73825f" 62 | integrity sha512-dNhchlN/3k3EKtyPXWdEEI7wiPQ07WInzsklWFm/6dUZspF63nj0O1A9PTsMxbmtQziwnOOl5oR+BgWNjTEYLA== 63 | optionalDependencies: 64 | fsevents "~2.3.2" 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # useRefs ♻️ 2 | 3 | > React hook to create multiple refs in a single call. 4 | 5 | ## Usage 6 | 7 | Instead of calling `useRef` many times, you just call `useRefs` and destructure **as many refs as you want!** 8 | 9 | ```js 10 | import useRefs from "react-use-refs"; 11 | 12 | const [someRef, anotherRef] = useRefs(); 13 | ``` 14 | 15 | The first and only argument is the `initialValue` of the refs. There’s no way to specify a different value for each ref. Use a list of `React.useRef`s for that! 16 | 17 | ## TypeScript support 18 | 19 | This library supports TypeScript and should work exactly as `React.useRef` does. 20 | 21 | ```tsx 22 | const [cardRef, cardBodyRef] = useRefs(); 23 | 24 | return
...
; 25 | ``` 26 | 27 | If you want to have multiple ref types, then you can pass a tuple and have it spread onto the created refs: 28 | 29 | ```tsx 30 | const [cardRef, inputRef] = useRefs<[HTMLDivElement, HTMLInputElement]>(null); 31 | ``` 32 | 33 | > ⚠️ Passing `null` as the `initialValue` is required for tuples! 34 | 35 | ## Frequently Asked Questions 36 | 37 | #### 🧙‍♀️ Is this black magic? 38 | 39 | No. Keep reading. 40 | 41 | #### 🤔 So how does this work?! 42 | 43 | This hook returns an iterable object, something you can use in a `for (of)` statement, with `Array.from()` or (and this is the neat part) with the Array Destructuring syntax (the one you use for `state`/`setState` pairs for example.) 44 | 45 | Also, if you have a look at the code the returned value from the iterator has always the `done` flag set to `false`. This is an infinite loop disguised as an iterator! But since calling next during array destructuring happens a finite number of times, hence we do not hit typical infinte loop behaviour (aka frozen page). 46 | 47 | #### 📜 Does this break the Rules of Hooks? 48 | 49 | **Short answer: no.** Real answer: it’s up to you. Actual real answer follows. 50 | 51 | The [_Rules of Hooks_ section of React’s official documentation cite](https://reactjs.org/docs/hooks-rules.html): Don’t call Hooks inside loops, conditions, or nested functions. 52 | 53 | As you can see in the source code we are definitely breaking this rule by calling `useRef` inside the `next()` method of the Iterable. 54 | 55 | But we need to understand the the Rules of Hooks exist for a reason, and that is to have statically stable invocation of primitive hooks between re-renders. 56 | 57 | Since we explicitly encourage the use of Array Destructuring, the dynamic part is made “static” by hard-coding it in your own source code. We therefore **do not** break the rules of hooks. 58 | 59 | #### 😈 But wait I can manually call `.next()` conditionally! 60 | 61 | Yeah, you can do some bad stuff with the returned iterator, but it’s not that different from having, for example, the following code: 62 | 63 | ```js 64 | const iSwearIAmNotUseRef = React.useRef(); 65 | ``` 66 | 67 | The only issue is that using the returned iterator doesn’t throw a ESLint warning at you as the above code would. 68 | 69 | #### 😕 Ok, but why did you do it? 70 | 71 | Because I could. And because [@drcmda](https://github.com/drcmda) said he would use it and would love it. ❤️ Spread love and not some silly questions about what people do in their free time. 72 | 73 | ## Credits 74 | 75 | - Thanks to [@drcmda](https://github.com/drcmda) for stating his need for such an API. 76 | - Thanks to [@Andarist](https://github.com/Andarist) for the initial TypeScript types definition. 77 | 78 | ## License 79 | 80 | MIT 81 | --------------------------------------------------------------------------------