├── .babelrc ├── .commitlintrc.json ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .nvmrc ├── .travis.yml ├── LICENSE ├── README.md ├── example ├── components │ ├── Checkbox.tsx │ └── Input.tsx ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages │ ├── _document.tsx │ └── index.tsx ├── tsconfig.json └── yarn.lock ├── package.json ├── release.sh ├── rollup.config.js ├── src ├── index.ts ├── nextId.test.ts ├── nextId.ts ├── useId.test.tsx └── useId.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/typescript", "@babel/env", "@babel/react"] 3 | } 4 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .next 4 | lib -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended", 5 | "plugin:@typescript-eslint/eslint-recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "prettier" 8 | ], 9 | "parser": "@typescript-eslint/parser", 10 | "env": { 11 | "jest": true, 12 | "node": true, 13 | "browser": true 14 | }, 15 | "plugins": ["@typescript-eslint"], 16 | "rules": { 17 | "@typescript-eslint/explicit-member-accessibility": "off", 18 | "@typescript-eslint/explicit-function-return-type": "off" 19 | }, 20 | "settings": { 21 | "react": { 22 | "version": "detect" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | .next 4 | .vscode 5 | package-lock.json 6 | lib 7 | build -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v15 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | script: 5 | - yarn lint 6 | - yarn typecheck 7 | - yarn test 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tomek 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.md: -------------------------------------------------------------------------------- 1 | # react-id-generator [![npm version][npm-badge]][npm-link] [![Build Status][ci-badge]][ci-link] [![ts][ts-badge]][ts-link] 2 | 3 | Generate unique id's in React components (e.g. for accessibility). 4 | 5 | **Features:** 6 | 7 | - Generates unique but predictable id's ✔︎ 8 | - Works with server-side rendering ✔︎ 9 | - TypeScript support ✔︎ 10 | 11 | See an example with [Next.js](https://nextjs.org/) app: 12 |
13 | [![Edit react-id-generator-example][cs-button]](https://codesandbox.io/s/react-id-generator-example-udjzm?fontsize=14) 14 | 15 | ### Basic example: 16 | 17 | ```jsx 18 | import React from "react"; 19 | import nextId from "react-id-generator"; 20 | 21 | class RadioButton extends React.Component { 22 | htmlId = nextId(); 23 | 24 | render() { 25 | const { children, ...rest } = this.props; 26 | return ( 27 |
28 | 29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | // Or with hooks: 36 | import React from "react"; 37 | import { useId } from "react-id-generator"; 38 | 39 | const RadioButton = ({ children, ...rest }) => { 40 | const [htmlId] = useId(); 41 | 42 | return ( 43 |
44 | 45 | 46 |
47 | ); 48 | }; 49 | ``` 50 | 51 | Each instance of `RadioButton` will have unique `htmlId` like: _id-1_, _id-2_, _id-3_, _id-4_ and so on. 52 | 53 | ### `nextId` 54 | 55 | This is simple function that returns unique id that's incrementing on each call. It can take an argument which will be used as prefix: 56 | 57 | ```js 58 | import nextId from "react-id-generator"; 59 | 60 | const id1 = nextId(); // id: id-1 61 | const id2 = nextId("test-id-"); // id: test-id-2 62 | const id3 = nextId(); // id: id-3 63 | ``` 64 | 65 | NOTE: Don't initialize `htmlId` in React lifecycle methods like _render()_. `htmlId` should stay the same during component lifetime. 66 | 67 | ### `useId` 68 | 69 | This is a hook that will generate id (or id's) which will stay the same across re-renders - it's a function component equivalent of `nextId`. However, with some additional features. 70 | 71 | By default it will return an array with single element: 72 | 73 | ```jsx 74 | const idList = useId(); // idList: ["id1"] 75 | ``` 76 | 77 | but you can specify how many id's it should return: 78 | 79 | ```jsx 80 | const idList = useId(3); // idList: ["id1", "id2", "id3"] 81 | ``` 82 | 83 | you can also set a prefix for them: 84 | 85 | ```jsx 86 | const idList = useId(3, "test"); // idList: ["test1", "test2", "test3"] 87 | ``` 88 | 89 | **New id's will be generated only when one of the arguments change.** 90 | 91 | ### `resetId` 92 | 93 | This function will reset the id counter. Main purpose of this function is to avoid warnings thrown by React durring server-side rendering (and also avoid counter exceeding `Number.MAX_SAFE_INTEGER`): 94 | 95 | > Warning: Prop `id` did not match. Server: "test-5" Client: "test-1" 96 | 97 | While in browser generator will always start from "1", durring SSR we need to manually reset it before generating markup for client: 98 | 99 | ```javascript 100 | import { resetId } from "react-id-generator"; 101 | 102 | server.get("*", (req, res) => { 103 | resetId(); 104 | 105 | const reactApp = ( 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | ); 114 | const html = renderToString(reactApp); 115 | 116 | res.render("index", { html }); 117 | } 118 | ``` 119 | 120 | This should keep ids in sync both in server and browser generated markup. 121 | 122 | ### `setPrefix` 123 | 124 | You can set prefix globally for every future id that will be generated: 125 | 126 | ```javascript 127 | import { setPrefix } from "react-id-generator"; 128 | 129 | setPrefix("test-id-"); 130 | 131 | const id1 = nextId(); // id: test-id-1 132 | const id2 = nextId(); // id: test-id-2 133 | const id3 = nextId("local"); // id: local-3 - note that local prefix has precedence 134 | ``` 135 | 136 | ### Running example in the repo: 137 | 138 | 1. First build the package: `yarn build && yarn build:declarations` 139 | 2. Go to `example/` directory and run `yarn dev` 140 | 141 |
142 | 143 | Props go to people that shared their ideas in [this SO topic](https://stackoverflow.com/q/29420835/4443323). 144 | 145 | [npm-badge]: https://badge.fury.io/js/react-id-generator.svg 146 | [npm-link]: https://badge.fury.io/js/react-id-generator 147 | [ci-badge]: https://travis-ci.org/Tomekmularczyk/react-id-generator.svg?branch=master 148 | [ci-link]: https://travis-ci.org/Tomekmularczyk/react-id-generator 149 | [ts-badge]: https://badges.frapsoft.com/typescript/code/typescript.svg?v=101 150 | [ts-link]: https://www.typescriptlang.org/ 151 | [cs-button]: https://codesandbox.io/static/img/play-codesandbox.svg 152 | -------------------------------------------------------------------------------- /example/components/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useId } from "../../lib"; 3 | 4 | const Checkbox: React.FC = () => { 5 | const [id1] = useId(); 6 | return ( 7 |
8 | 9 | 10 |
11 | ); 12 | }; 13 | 14 | export default Checkbox; 15 | -------------------------------------------------------------------------------- /example/components/Input.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import nextId from "../../lib"; 3 | 4 | class Input extends React.Component { 5 | uniqueId = nextId(); 6 | 7 | render(): JSX.Element { 8 | return ( 9 |
10 | 11 |
12 | 13 |
14 | ); 15 | } 16 | } 17 | 18 | export default Input; 19 | -------------------------------------------------------------------------------- /example/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /example/next.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This resolves "Hooks can only be called inside the body of a function component." error 3 | * which happens when importing from lib/ directory outside of this example: 4 | * https://github.com/webpack/webpack/issues/8607#issuecomment-453068938 5 | */ 6 | // eslint-disable-next-line 7 | module.exports = { 8 | webpack: (config) => { 9 | config.resolve.alias = { 10 | ...config.resolve.alias, 11 | react: require.resolve("react"), 12 | }; 13 | return config; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "next", 8 | "build": "next build", 9 | "start": "next start" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@types/node": "^14.0.22", 16 | "@types/react": "^16.9.42", 17 | "@types/react-dom": "^16.9.8", 18 | "next": "12.1.0", 19 | "react": "^16.13.1", 20 | "react-dom": "^16.13.1", 21 | "typescript": "^3.9.6" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { DocumentContext, DocumentInitialProps } from "next/document"; 2 | import { resetId } from "../../lib"; 3 | 4 | class MyDocument extends Document { 5 | static async getInitialProps( 6 | ctx: DocumentContext 7 | ): Promise { 8 | // _document is only rendered on the server side and not on the client side 9 | // this will reset id keeping markup consistent across server and browser 10 | resetId(); 11 | 12 | const initialProps = await Document.getInitialProps(ctx); 13 | return { ...initialProps }; 14 | } 15 | } 16 | 17 | export default MyDocument; 18 | -------------------------------------------------------------------------------- /example/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Input from "../components/Input"; 3 | import Checkbox from "../components/Checkbox"; 4 | import { setPrefix } from "../../lib"; 5 | 6 | setPrefix("test-"); 7 | 8 | const IndexPage: React.FC = () => ( 9 |
10 |

11 | Try to refresh page and look to the console - it's clear!
12 | No mismatch between id's generated in server and in browser. 13 |

14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 |
22 | ); 23 | 24 | export default IndexPage; 25 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "alwaysStrict": true, 5 | "esModuleInterop": true, 6 | "isolatedModules": true, 7 | "jsx": "preserve", 8 | "lib": ["dom", "es2017"], 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noEmit": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "strict": true, 16 | "target": "esnext", 17 | "skipLibCheck": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "resolveJsonModule": true 20 | }, 21 | "exclude": ["node_modules"], 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] 23 | } 24 | -------------------------------------------------------------------------------- /example/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@next/env@12.1.0": 6 | version "12.1.0" 7 | resolved "https://registry.yarnpkg.com/@next/env/-/env-12.1.0.tgz#73713399399b34aa5a01771fb73272b55b22c314" 8 | integrity sha512-nrIgY6t17FQ9xxwH3jj0a6EOiQ/WDHUos35Hghtr+SWN/ntHIQ7UpuvSi0vaLzZVHQWaDupKI+liO5vANcDeTQ== 9 | 10 | "@next/swc-android-arm64@12.1.0": 11 | version "12.1.0" 12 | resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.1.0.tgz#865ba3a9afc204ff2bdeea49dd64d58705007a39" 13 | integrity sha512-/280MLdZe0W03stA69iL+v6I+J1ascrQ6FrXBlXGCsGzrfMaGr7fskMa0T5AhQIVQD4nA/46QQWxG//DYuFBcA== 14 | 15 | "@next/swc-darwin-arm64@12.1.0": 16 | version "12.1.0" 17 | resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.0.tgz#08e8b411b8accd095009ed12efbc2f1d4d547135" 18 | integrity sha512-R8vcXE2/iONJ1Unf5Ptqjk6LRW3bggH+8drNkkzH4FLEQkHtELhvcmJwkXcuipyQCsIakldAXhRbZmm3YN1vXg== 19 | 20 | "@next/swc-darwin-x64@12.1.0": 21 | version "12.1.0" 22 | resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.0.tgz#fcd684497a76e8feaca88db3c394480ff0b007cd" 23 | integrity sha512-ieAz0/J0PhmbZBB8+EA/JGdhRHBogF8BWaeqR7hwveb6SYEIJaDNQy0I+ZN8gF8hLj63bEDxJAs/cEhdnTq+ug== 24 | 25 | "@next/swc-linux-arm-gnueabihf@12.1.0": 26 | version "12.1.0" 27 | resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.0.tgz#9ec6380a27938a5799aaa6035c205b3c478468a7" 28 | integrity sha512-njUd9hpl6o6A5d08dC0cKAgXKCzm5fFtgGe6i0eko8IAdtAPbtHxtpre3VeSxdZvuGFh+hb0REySQP9T1ttkog== 29 | 30 | "@next/swc-linux-arm64-gnu@12.1.0": 31 | version "12.1.0" 32 | resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.0.tgz#7f4196dff1049cea479607c75b81033ae2dbd093" 33 | integrity sha512-OqangJLkRxVxMhDtcb7Qn1xjzFA3s50EIxY7mljbSCLybU+sByPaWAHY4px97ieOlr2y4S0xdPKkQ3BCAwyo6Q== 34 | 35 | "@next/swc-linux-arm64-musl@12.1.0": 36 | version "12.1.0" 37 | resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.0.tgz#b445f767569cdc2dddee785ca495e1a88c025566" 38 | integrity sha512-hB8cLSt4GdmOpcwRe2UzI5UWn6HHO/vLkr5OTuNvCJ5xGDwpPXelVkYW/0+C3g5axbDW2Tym4S+MQCkkH9QfWA== 39 | 40 | "@next/swc-linux-x64-gnu@12.1.0": 41 | version "12.1.0" 42 | resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.0.tgz#67610e9be4fbc987de7535f1bcb17e45fe12f90e" 43 | integrity sha512-OKO4R/digvrVuweSw/uBM4nSdyzsBV5EwkUeeG4KVpkIZEe64ZwRpnFB65bC6hGwxIBnTv5NMSnJ+0K/WmG78A== 44 | 45 | "@next/swc-linux-x64-musl@12.1.0": 46 | version "12.1.0" 47 | resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.0.tgz#ea19a23db08a9f2e34ac30401f774cf7d1669d31" 48 | integrity sha512-JohhgAHZvOD3rQY7tlp7NlmvtvYHBYgY0x5ZCecUT6eCCcl9lv6iV3nfu82ErkxNk1H893fqH0FUpznZ/H3pSw== 49 | 50 | "@next/swc-win32-arm64-msvc@12.1.0": 51 | version "12.1.0" 52 | resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.0.tgz#eadf054fc412085659b98e145435bbba200b5283" 53 | integrity sha512-T/3gIE6QEfKIJ4dmJk75v9hhNiYZhQYAoYm4iVo1TgcsuaKLFa+zMPh4056AHiG6n9tn2UQ1CFE8EoybEsqsSw== 54 | 55 | "@next/swc-win32-ia32-msvc@12.1.0": 56 | version "12.1.0" 57 | resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.0.tgz#68faeae10c89f698bf9d28759172b74c9c21bda1" 58 | integrity sha512-iwnKgHJdqhIW19H9PRPM9j55V6RdcOo6rX+5imx832BCWzkDbyomWnlzBfr6ByUYfhohb8QuH4hSGEikpPqI0Q== 59 | 60 | "@next/swc-win32-x64-msvc@12.1.0": 61 | version "12.1.0" 62 | resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.0.tgz#d27e7e76c87a460a4da99c5bfdb1618dcd6cd064" 63 | integrity sha512-aBvcbMwuanDH4EMrL2TthNJy+4nP59Bimn8egqv6GHMVj0a44cU6Au4PjOhLNqEh9l+IpRGBqMTzec94UdC5xg== 64 | 65 | "@types/node@^14.0.22": 66 | version "14.0.22" 67 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.22.tgz#23ea4d88189cec7d58f9e6b66f786b215eb61bdc" 68 | integrity sha512-emeGcJvdiZ4Z3ohbmw93E/64jRzUHAItSHt8nF7M4TGgQTiWqFVGB8KNpLGFmUHmHLvjvBgFwVlqNcq+VuGv9g== 69 | 70 | "@types/prop-types@*": 71 | version "15.7.1" 72 | resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.1.tgz#f1a11e7babb0c3cad68100be381d1e064c68f1f6" 73 | integrity sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg== 74 | 75 | "@types/react-dom@^16.9.8": 76 | version "16.9.8" 77 | resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423" 78 | integrity sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA== 79 | dependencies: 80 | "@types/react" "*" 81 | 82 | "@types/react@*": 83 | version "16.8.23" 84 | resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.23.tgz#ec6be3ceed6353a20948169b6cb4c97b65b97ad2" 85 | integrity sha512-abkEOIeljniUN9qB5onp++g0EY38h7atnDHxwKUFz1r3VH1+yG1OKi2sNPTyObL40goBmfKFpdii2lEzwLX1cA== 86 | dependencies: 87 | "@types/prop-types" "*" 88 | csstype "^2.2.0" 89 | 90 | "@types/react@^16.9.42": 91 | version "16.9.42" 92 | resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.42.tgz#9776508d59c1867bbf9bd7f036dab007fdaa1cb7" 93 | integrity sha512-iGy6HwfVfotqJ+PfRZ4eqPHPP5NdPZgQlr0lTs8EfkODRBV9cYy8QMKcC9qPCe1JrESC1Im6SrCFR6tQgg74ag== 94 | dependencies: 95 | "@types/prop-types" "*" 96 | csstype "^2.2.0" 97 | 98 | caniuse-lite@^1.0.30001283: 99 | version "1.0.30001312" 100 | resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz#e11eba4b87e24d22697dae05455d5aea28550d5f" 101 | integrity sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ== 102 | 103 | csstype@^2.2.0: 104 | version "2.6.6" 105 | resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.6.tgz#c34f8226a94bbb10c32cc0d714afdf942291fc41" 106 | integrity sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg== 107 | 108 | "js-tokens@^3.0.0 || ^4.0.0": 109 | version "4.0.0" 110 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 111 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 112 | 113 | loose-envify@^1.1.0, loose-envify@^1.4.0: 114 | version "1.4.0" 115 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" 116 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== 117 | dependencies: 118 | js-tokens "^3.0.0 || ^4.0.0" 119 | 120 | nanoid@^3.1.30: 121 | version "3.3.1" 122 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" 123 | integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== 124 | 125 | next@12.1.0: 126 | version "12.1.0" 127 | resolved "https://registry.yarnpkg.com/next/-/next-12.1.0.tgz#c33d753b644be92fc58e06e5a214f143da61dd5d" 128 | integrity sha512-s885kWvnIlxsUFHq9UGyIyLiuD0G3BUC/xrH0CEnH5lHEWkwQcHOORgbDF0hbrW9vr/7am4ETfX4A7M6DjrE7Q== 129 | dependencies: 130 | "@next/env" "12.1.0" 131 | caniuse-lite "^1.0.30001283" 132 | postcss "8.4.5" 133 | styled-jsx "5.0.0" 134 | use-subscription "1.5.1" 135 | optionalDependencies: 136 | "@next/swc-android-arm64" "12.1.0" 137 | "@next/swc-darwin-arm64" "12.1.0" 138 | "@next/swc-darwin-x64" "12.1.0" 139 | "@next/swc-linux-arm-gnueabihf" "12.1.0" 140 | "@next/swc-linux-arm64-gnu" "12.1.0" 141 | "@next/swc-linux-arm64-musl" "12.1.0" 142 | "@next/swc-linux-x64-gnu" "12.1.0" 143 | "@next/swc-linux-x64-musl" "12.1.0" 144 | "@next/swc-win32-arm64-msvc" "12.1.0" 145 | "@next/swc-win32-ia32-msvc" "12.1.0" 146 | "@next/swc-win32-x64-msvc" "12.1.0" 147 | 148 | object-assign@^4.1.1: 149 | version "4.1.1" 150 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 151 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 152 | 153 | picocolors@^1.0.0: 154 | version "1.0.0" 155 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" 156 | integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== 157 | 158 | postcss@8.4.5: 159 | version "8.4.5" 160 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95" 161 | integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg== 162 | dependencies: 163 | nanoid "^3.1.30" 164 | picocolors "^1.0.0" 165 | source-map-js "^1.0.1" 166 | 167 | prop-types@^15.6.2: 168 | version "15.7.2" 169 | resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" 170 | integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== 171 | dependencies: 172 | loose-envify "^1.4.0" 173 | object-assign "^4.1.1" 174 | react-is "^16.8.1" 175 | 176 | react-dom@^16.13.1: 177 | version "16.13.1" 178 | resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" 179 | integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag== 180 | dependencies: 181 | loose-envify "^1.1.0" 182 | object-assign "^4.1.1" 183 | prop-types "^15.6.2" 184 | scheduler "^0.19.1" 185 | 186 | react-is@^16.8.1: 187 | version "16.8.6" 188 | resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" 189 | integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== 190 | 191 | react@^16.13.1: 192 | version "16.13.1" 193 | resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" 194 | integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== 195 | dependencies: 196 | loose-envify "^1.1.0" 197 | object-assign "^4.1.1" 198 | prop-types "^15.6.2" 199 | 200 | scheduler@^0.19.1: 201 | version "0.19.1" 202 | resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" 203 | integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== 204 | dependencies: 205 | loose-envify "^1.1.0" 206 | object-assign "^4.1.1" 207 | 208 | source-map-js@^1.0.1: 209 | version "1.0.2" 210 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" 211 | integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== 212 | 213 | styled-jsx@5.0.0: 214 | version "5.0.0" 215 | resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.0.tgz#816b4b92e07b1786c6b7111821750e0ba4d26e77" 216 | integrity sha512-qUqsWoBquEdERe10EW8vLp3jT25s/ssG1/qX5gZ4wu15OZpmSMFI2v+fWlRhLfykA5rFtlJ1ME8A8pm/peV4WA== 217 | 218 | typescript@^3.9.6: 219 | version "3.9.6" 220 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a" 221 | integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw== 222 | 223 | use-subscription@1.5.1: 224 | version "1.5.1" 225 | resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1" 226 | integrity sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA== 227 | dependencies: 228 | object-assign "^4.1.1" 229 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-id-generator", 3 | "version": "3.0.2", 4 | "description": "Simple and universal HTML-id generator for React.", 5 | "repository": "https://github.com/Tomekmularczyk/react-id-generator.git", 6 | "main": "lib/index.js", 7 | "files": [ 8 | "lib" 9 | ], 10 | "types": "./lib/index.d.ts", 11 | "scripts": { 12 | "dev": "concurrently \"yarn build:dev --watch\" \"yarn build:declarations --watch\"", 13 | "build:dev": "rollup -c", 14 | "build": "NODE_ENV=production rollup -c", 15 | "build:declarations": "tsc --declaration --emitDeclarationOnly --declarationDir lib", 16 | "lint": "eslint --ext .ts,.tsx .", 17 | "typecheck": "tsc --noEmit", 18 | "test": "jest", 19 | "prepare": "husky install" 20 | }, 21 | "keywords": [ 22 | "id", 23 | "react", 24 | "react-id", 25 | "id-generator" 26 | ], 27 | "author": "Tomasz Mularczyk", 28 | "license": "MIT", 29 | "devDependencies": { 30 | "@babel/core": "^7.15.5", 31 | "@babel/preset-env": "^7.15.4", 32 | "@babel/preset-react": "^7.14.5", 33 | "@babel/preset-typescript": "^7.15.0", 34 | "@commitlint/cli": "^13.1.0", 35 | "@commitlint/config-conventional": "^13.1.0", 36 | "@types/jest": "^27.0.1", 37 | "@types/react": "^16.9.42", 38 | "@types/react-dom": "^16.9.8", 39 | "@typescript-eslint/eslint-plugin": "^4.31.0", 40 | "@typescript-eslint/parser": "^4.31.0", 41 | "concurrently": "^6.2.1", 42 | "eslint": "^7.32.0", 43 | "eslint-config-prettier": "^8.3.0", 44 | "eslint-plugin-react": "^7.25.1", 45 | "husky": "^7.0.0", 46 | "jest": "^27.1.0", 47 | "lint-staged": "^11.1.2", 48 | "prettier": "^2.3.2", 49 | "react": "^16.10.2", 50 | "react-dom": "^16.10.2", 51 | "rollup": "^2.56.3", 52 | "rollup-plugin-babel": "^4.4.0", 53 | "rollup-plugin-commonjs": "^10.1.0", 54 | "rollup-plugin-node-resolve": "^5.2.0", 55 | "typescript": "^4.4.2" 56 | }, 57 | "peerDependencies": { 58 | "react": ">= 16.8.0" 59 | }, 60 | "lint-staged": { 61 | "*.{js,ts,tsx}": "eslint", 62 | "*.{js,ts,tsx,css,md}": "prettier --write" 63 | }, 64 | "jest": { 65 | "testEnvironment": "jsdom" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### CONSTANTS 4 | GREEN='\033[0;32m' 5 | RED='\033[91m' 6 | NC='\033[0m' # No Color 7 | 8 | ### FUNCTIONS 9 | replaceVersionInPackageJSON() { 10 | local NEW_VERSION=$1 11 | local NEW_VERSION_LINE=" \"version\": \"$NEW_VERSION\"," 12 | sed -i '' "s/.*\"version\":.*/$NEW_VERSION_LINE/" package.json || exit 1; 13 | } 14 | 15 | printCurrentVersion() { 16 | local VERSION=$(sed '3q;d' package.json | xargs) # expected on 3rd line 17 | echo "Current${GREEN} $VERSION ${NC}" 18 | } 19 | 20 | readNewVersion() { 21 | local NEW_VERSION 22 | read -p "Type new version: " NEW_VERSION 23 | echo $NEW_VERSION 24 | } 25 | 26 | loginToNPM() { 27 | echo "Loging to npm..." 28 | npm login || exit 1; 29 | } 30 | 31 | runTests() { 32 | echo 'Running tests...\n' 33 | yarn lint || exit 1; 34 | yarn typecheck || exit 1; 35 | yarn test --silent --noStackTrace --colors >/dev/null || exit 1; 36 | echo '\n' 37 | } 38 | 39 | buildPackage() { 40 | echo 'Building library...\n' 41 | rm -rf lib 42 | yarn build || exit 1; 43 | yarn build:declarations || exit 1; 44 | } 45 | 46 | bailoutIfRepoIsNotClean() { 47 | git diff-index --quiet HEAD -- \ 48 | || { echo "${RED}You have uncommitted changes, clean up the repo first.${NC}"; exit 1; } 49 | } 50 | 51 | confirmNewVersion() { 52 | local NEW_VERSION=$1 53 | local CONFIRMED 54 | echo "Are you sure you want to create version${GREEN} $NEW_VERSION ${NC}?" 55 | read -p "(y/n): " CONFIRMED 56 | if [ $CONFIRMED != "y" ] && [ $CONFIRMED != "Y" ] 57 | then 58 | exit 1; 59 | fi 60 | } 61 | 62 | confirmMasterBranch() { 63 | local CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) 64 | if [ $CURRENT_BRANCH != "master" ] 65 | then 66 | echo "${RED}First switch to master branch.${NC}" 67 | exit 1; 68 | fi 69 | } 70 | 71 | commitAndCreateTag() { 72 | local NEW_VERSION=$1 73 | git add --all 74 | git commit -m "chore: release version $NEW_VERSION" || exit 1; 75 | git tag "v$NEW_VERSION" 76 | } 77 | 78 | pushToRemote() { 79 | local CONFIRMED 80 | echo "Do you want to push new commit and tag to remote repo?" 81 | read -p "(y/n): " CONFIRMED 82 | if [ $CONFIRMED = "y" ] || [ $CONFIRMED = "Y" ] 83 | then 84 | git push origin master 85 | git push origin --tags 86 | fi 87 | } 88 | 89 | confirmMasterBranch 90 | 91 | bailoutIfRepoIsNotClean 92 | 93 | clear 94 | 95 | loginToNPM 96 | 97 | runTests 98 | 99 | buildPackage 100 | 101 | printCurrentVersion 102 | 103 | NEW_VERSION=$(readNewVersion) 104 | 105 | confirmNewVersion $NEW_VERSION 106 | 107 | replaceVersionInPackageJSON $NEW_VERSION 108 | 109 | commitAndCreateTag $NEW_VERSION 110 | 111 | npm publish 112 | 113 | pushToRemote 114 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from "rollup-plugin-babel"; 2 | import resolve from "rollup-plugin-node-resolve"; 3 | import commonjs from "rollup-plugin-commonjs"; 4 | 5 | const extensions = [".ts", ".tsx"]; 6 | 7 | export default { 8 | input: "./src/index.ts", 9 | output: { 10 | file: "./lib/index.js", 11 | format: "cjs", 12 | exports: "named", 13 | }, 14 | plugins: [ 15 | babel({ 16 | exclude: "node_modules/**", 17 | extensions, 18 | include: ["src/**/*"], 19 | }), 20 | resolve({ extensions }), 21 | commonjs(), 22 | ], 23 | external: ["react", "react-dom"], 24 | }; 25 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import nextId, { resetId, setPrefix } from "./nextId"; 2 | import useId from "./useId"; 3 | 4 | export { nextId as default, resetId, setPrefix, useId }; 5 | -------------------------------------------------------------------------------- /src/nextId.test.ts: -------------------------------------------------------------------------------- 1 | import nextId, { resetId, setPrefix } from "./nextId"; 2 | 3 | afterEach(() => { 4 | resetId(); 5 | setPrefix("id"); 6 | }); 7 | 8 | describe("nextId", () => { 9 | it("generates unique id", () => { 10 | for (let i = 1; i < 10; i++) { 11 | expect(nextId()).toBe(`id${i}`); 12 | } 13 | }); 14 | 15 | it("takes global prefix", () => { 16 | setPrefix("test-"); 17 | expect(nextId()).toBe("test-1"); 18 | expect(nextId()).toBe("test-2"); 19 | setPrefix("abc@"); 20 | expect(nextId()).toBe("abc@3"); 21 | }); 22 | 23 | it("takes prefix", () => { 24 | expect(nextId("test-")).toBe("test-1"); 25 | expect(nextId("test-")).toBe("test-2"); 26 | expect(nextId("abc@")).toBe("abc@3"); 27 | }); 28 | 29 | it("takes local prefix over global prefix", () => { 30 | setPrefix("test-"); 31 | expect(nextId()).toBe("test-1"); 32 | expect(nextId("abc@")).toBe("abc@2"); 33 | }); 34 | }); 35 | 36 | describe("resetId", () => { 37 | it("resets the id", () => { 38 | for (let i = 1; i < 10; i++) { 39 | resetId(); 40 | expect(nextId()).toBe("id1"); 41 | } 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/nextId.ts: -------------------------------------------------------------------------------- 1 | let globalPrefix = "id"; 2 | let lastId = 0; 3 | export default function nextId(localPrefix?: string | null): string { 4 | lastId++; 5 | return `${localPrefix || globalPrefix}${lastId}`; 6 | } 7 | 8 | export const resetId = (): void => { 9 | lastId = 0; 10 | }; 11 | 12 | export const setPrefix = (newPrefix: string): void => { 13 | globalPrefix = newPrefix; 14 | }; 15 | -------------------------------------------------------------------------------- /src/useId.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { renderIntoDocument } from "react-dom/test-utils"; 4 | import useId from "./useId"; 5 | import { resetId } from "./nextId"; 6 | 7 | describe("useId", () => { 8 | let container: HTMLDivElement | null; 9 | 10 | beforeEach(() => { 11 | resetId(); 12 | container = document.createElement("div"); 13 | document.body.appendChild(container); 14 | }); 15 | 16 | afterEach(() => { 17 | if (container) { 18 | document.body.removeChild(container); 19 | container = null; 20 | } 21 | }); 22 | 23 | it("generates single id with no arguments", () => { 24 | let idList: string[] = []; 25 | const Component = () => { 26 | idList = useId(); 27 | return null; 28 | }; 29 | 30 | renderIntoDocument(); 31 | 32 | expect(idList.length).toBe(1); 33 | expect(idList[0]).toBe("id1"); 34 | }); 35 | 36 | it("generates more ids when passing count", () => { 37 | let idList: string[] = []; 38 | const Component = () => { 39 | idList = useId(3); 40 | return null; 41 | }; 42 | 43 | renderIntoDocument(); 44 | 45 | expect(idList.length).toBe(3); 46 | expect(idList[0]).toBe("id1"); 47 | expect(idList[1]).toBe("id2"); 48 | expect(idList[2]).toBe("id3"); 49 | }); 50 | 51 | it("takes prefix", () => { 52 | let idList: string[] = []; 53 | const Component = () => { 54 | idList = useId(1, "test-"); 55 | return null; 56 | }; 57 | 58 | renderIntoDocument(); 59 | 60 | expect(idList.length).toBe(1); 61 | expect(idList[0]).toBe("test-1"); 62 | }); 63 | 64 | it("returns new id's list when count changes", () => { 65 | let idList: string[] = []; 66 | const Component = ({ idsCount }: { idsCount: number }) => { 67 | idList = useId(idsCount); 68 | return null; 69 | }; 70 | 71 | ReactDOM.render(, container); 72 | expect(idList.length).toBe(1); 73 | expect(idList[0]).toBe("id1"); 74 | 75 | ReactDOM.render(, container); 76 | expect(idList.length).toBe(2); 77 | expect(idList[0]).toBe("id2"); 78 | expect(idList[1]).toBe("id3"); 79 | 80 | // nothing had changed 81 | ReactDOM.render(, container); 82 | expect(idList.length).toBe(2); 83 | expect(idList[0]).toBe("id2"); 84 | expect(idList[1]).toBe("id3"); 85 | 86 | ReactDOM.render(, container); 87 | 88 | expect(idList.length).toBe(1); 89 | expect(idList[0]).toBe("id4"); 90 | }); 91 | 92 | it("returns new id's list when prefix changes", () => { 93 | let idList: string[] = []; 94 | const Component = ({ prefix }: { prefix: string }) => { 95 | idList = useId(1, prefix); 96 | return null; 97 | }; 98 | 99 | ReactDOM.render(, container); 100 | expect(idList.length).toBe(1); 101 | expect(idList[0]).toBe("a-1"); 102 | 103 | ReactDOM.render(, container); 104 | expect(idList.length).toBe(1); 105 | expect(idList[0]).toBe("b-2"); 106 | 107 | // nothing had changed 108 | ReactDOM.render(, container); 109 | expect(idList.length).toBe(1); 110 | expect(idList[0]).toBe("b-2"); 111 | 112 | ReactDOM.render(, container); 113 | 114 | expect(idList.length).toBe(1); 115 | expect(idList[0]).toBe("c-3"); 116 | }); 117 | 118 | it("returns new id's immediately when dependencies change (in the same render)", () => { 119 | const idsRenderHistory: string[][] = []; 120 | const Component = ({ idsCount }: { idsCount: number }) => { 121 | const ids = useId(idsCount); 122 | idsRenderHistory.push(ids); 123 | return null; 124 | }; 125 | 126 | ReactDOM.render(, container); 127 | expect(idsRenderHistory).toHaveLength(1); 128 | expect(idsRenderHistory[0]).toEqual(["id1", "id2"]); 129 | 130 | ReactDOM.render(, container); 131 | expect(idsRenderHistory).toHaveLength(2); 132 | expect(idsRenderHistory[1]).toEqual(["id3", "id4", "id5"]); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /src/useId.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import nextId from "./nextId"; 3 | 4 | const getIds = (count: number, prefix?: string) => { 5 | const ids = []; 6 | for (let i = 0; i < count; i++) { 7 | ids.push(nextId(prefix)); 8 | } 9 | return ids; 10 | }; 11 | 12 | function usePrevious(value: unknown) { 13 | const ref = React.useRef(); 14 | React.useEffect(() => { 15 | ref.current = value; 16 | }); 17 | return ref.current; 18 | } 19 | 20 | export default function useId(count = 1, prefix?: string): string[] { 21 | const idsListRef = React.useRef([]); 22 | const prevCount = usePrevious(count); 23 | const prevPrefix = usePrevious(prefix); 24 | 25 | if (count !== prevCount || prevPrefix !== prefix) { 26 | idsListRef.current = getIds(count, prefix); 27 | } 28 | 29 | return idsListRef.current; 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "jsx": "react", 8 | "types": [], 9 | "noImplicitAny": true 10 | }, 11 | "include": ["src"], 12 | "exclude": ["node_modules", "**/*.test.*", "lib", "example"] 13 | } 14 | --------------------------------------------------------------------------------