├── .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 | {children}
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 | {children}
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 | Checkbox with id: {id1}
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 | Input with id: {this.uniqueId}
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 |
--------------------------------------------------------------------------------