├── .editorconfig ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── pull_request_template.md ├── .gitignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── UtilityJs-wide.png ├── package.json ├── scripts └── build-npm-module.js ├── src ├── data-structure │ ├── Comparator │ │ ├── Comparator.ts │ │ ├── README.md │ │ ├── index.ts │ │ └── package.json │ ├── DisjointSet │ │ ├── DisjointSet.ts │ │ ├── README.md │ │ ├── index.ts │ │ └── package.json │ ├── Graph │ │ ├── Graph.ts │ │ ├── README.md │ │ ├── index.ts │ │ └── package.json │ ├── Heap │ │ ├── Heap.ts │ │ ├── README.md │ │ ├── index.ts │ │ └── package.json │ ├── LinkedList │ │ ├── LinkedList.ts │ │ ├── README.md │ │ ├── index.ts │ │ └── package.json │ ├── MaxHeap │ │ ├── MaxHeap.ts │ │ ├── README.md │ │ ├── index.ts │ │ └── package.json │ ├── MinHeap │ │ ├── MinHeap.ts │ │ ├── README.md │ │ ├── index.ts │ │ └── package.json │ ├── PriorityQueue │ │ ├── PriorityQueue.ts │ │ ├── README.md │ │ ├── index.ts │ │ └── package.json │ ├── Queue │ │ ├── Queue.ts │ │ ├── README.md │ │ ├── index.ts │ │ └── package.json │ ├── Stack │ │ ├── README.md │ │ ├── Stack.ts │ │ ├── index.ts │ │ └── package.json │ └── Vector │ │ ├── README.md │ │ ├── Vector.ts │ │ ├── index.ts │ │ └── package.json ├── function │ └── createStoreContext │ │ ├── README.md │ │ ├── createStoreContext.tsx │ │ ├── index.ts │ │ └── package.json ├── hook │ ├── useControlledProp │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useControlledProp.ts │ ├── useCopyToClipboard │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useCopyToClipboard.ts │ ├── useDarkMode │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useDarkMode.ts │ ├── useDeterministicId │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useDeterministicId.ts │ ├── useEventListener │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useEventListener.ts │ ├── useForceRerender │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useForceRerender.ts │ ├── useForkedRefs │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useForkedRefs.ts │ ├── useGetLatest │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useGetLatest.ts │ ├── useGetScrollbarWidth │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useGetScrollbarWidth.ts │ ├── useHash │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useHash.ts │ ├── useHover │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useHover.ts │ ├── useImmutableArray │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useImmutableArray.ts │ ├── useIsInViewport │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useIsInViewport.ts │ ├── useIsMounted │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useIsMounted.ts │ ├── useIsServerHandoffComplete │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useIsServerHandoffComplete.ts │ ├── useIsomorphicLayoutEffect │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useIsomorphicLayoutEffect.ts │ ├── useKeybind │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useKeybind.ts │ ├── useLazyInitializedValue │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useLazyInitializedValue.ts │ ├── useLongPress │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useLongPress.ts │ ├── useMediaQuery │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useMediaQuery.ts │ ├── useMementoState │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ ├── use-memento-state-img.png │ │ └── useMementoState.ts │ ├── useOnChange │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useOnChange.ts │ ├── useOnOutsideClick │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useOnOutsideClick.ts │ ├── usePersistedState │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── usePersistedState.ts │ ├── usePreviousValue │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── usePreviousValue.ts │ ├── usePubSub │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── usePubSub.ts │ ├── useRegisterNodeRef │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useRegisterNodeRef.ts │ ├── useResizeSensor │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useResizeSensor.ts │ ├── useScrollGuard │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useScrollGuard.ts │ └── useSyncEffect │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── useSyncEffect.ts └── typedef │ └── index.d.ts ├── tsconfig.cjs.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const peerDeps = Object.keys(require("./package.json").peerDependencies); 2 | 3 | module.exports = { 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:react/recommended", 7 | "plugin:react-hooks/recommended", 8 | "plugin:import/recommended", 9 | "plugin:import/typescript", 10 | "prettier" 11 | ], 12 | env: { 13 | browser: true, 14 | es6: true, 15 | node: true, 16 | commonjs: true 17 | }, 18 | globals: { 19 | Atomics: "readonly", 20 | SharedArrayBuffer: "readonly", 21 | JSX: true 22 | }, 23 | plugins: [ 24 | "eslint-plugin-import", 25 | "eslint-plugin-react", 26 | "eslint-plugin-react-hooks", 27 | "@typescript-eslint/eslint-plugin" 28 | ], 29 | parser: "@typescript-eslint/parser", 30 | parserOptions: { 31 | sourceType: "module" 32 | }, 33 | rules: { 34 | "no-alert": "error", 35 | "no-console": "warn", 36 | "prefer-const": "error", 37 | "default-case": "warn", 38 | "react/react-in-jsx-scope": "off", 39 | "@typescript-eslint/no-unused-vars": [ 40 | "warn", 41 | { argsIgnorePattern: "^_", varsIgnorePattern: "^_" } 42 | ], 43 | "import/no-unresolved": ["error", { ignore: peerDeps }] 44 | }, 45 | overrides: [ 46 | { 47 | files: ["*.ts", "*.tsx", "*.d.ts"], 48 | extends: [ 49 | "plugin:@typescript-eslint/recommended", 50 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 51 | ], 52 | parserOptions: { 53 | sourceType: "module", 54 | project: ["./tsconfig.json"] 55 | } 56 | } 57 | ], 58 | settings: { 59 | react: { version: "detect" }, 60 | "import/resolver": { 61 | alias: { 62 | map: [ 63 | [ 64 | "@utilityjs/use-get-latest", 65 | "./src/hook/useGetLatest/useGetLatest.ts" 66 | ], 67 | [ 68 | "@utilityjs/use-event-listener", 69 | "./src/hook/useEventListener/useEventListener.ts" 70 | ], 71 | [ 72 | "@utilityjs/use-previous-value", 73 | "./src/hook/usePreviousValue/usePreviousValue.ts" 74 | ], 75 | [ 76 | "@utilityjs/use-get-scrollbar-width", 77 | "./src/hook/useGetScrollbarWidth/useGetScrollbarWidth.ts" 78 | ], 79 | [ 80 | "@utilityjs/use-register-node-ref", 81 | "./src/hook/useRegisterNodeRef/useRegisterNodeRef.ts" 82 | ], 83 | [ 84 | "@utilityjs/use-persisted-state", 85 | "./src/hook/usePersistedState/usePersistedState.ts" 86 | ], 87 | [ 88 | "@utilityjs/use-media-query", 89 | "./src/hook/useMediaQuery/useMediaQuery.ts" 90 | ], 91 | [ 92 | "@utilityjs/use-lazy-initialized-value", 93 | "./src/hook/useLazyInitializedValue/useLazyInitializedValue.ts" 94 | ], 95 | [ 96 | "@utilityjs/comparator", 97 | "./src/data-structure/Comparator/Comparator.ts" 98 | ], 99 | ["@utilityjs/heap", "./src/data-structure/Heap/Heap.ts"], 100 | ["@utilityjs/min-heap", "./src/data-structure/MinHeap/MinHeap.ts"], 101 | [ 102 | "@utilityjs/linked-list", 103 | "./src/data-structure/LinkedList/LinkedList.ts" 104 | ] 105 | ] 106 | } 107 | } 108 | } 109 | }; 110 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | - [ ] Related issues linked using `fixes #number` 7 | - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. 8 | - [ ] Documentation added 9 | - [ ] The linting passes -------------------------------------------------------------------------------- /.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 | 106 | .DS_STORE 107 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "packageManager": "yarn", 4 | "printWidth": 80, 5 | "semi": true, 6 | "singleQuote": false, 7 | "tabWidth": 2, 8 | "useEditorConfig": true, 9 | "trailingComma": "none", 10 | "arrowParens": "avoid" 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mostafa Shamsitabar 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 |
2 |

UtilityJS

3 |
4 | 5 | ## Contributing 6 | 7 | Please see our [CONTRIBUTING.md](https://github.com/mimshins/utilityjs/blob/main/CONTRIBUTING.md). 8 | 9 | ## Community 10 | 11 | The UtilityJS community can be found on [Github Discussions](https://github.com/mimshins/utilityjs/discussions), where you can ask questions, voice ideas, and share your projects. 12 | 13 | Our [Code of Conduct](https://github.com/mimshins/utilityjs/blob/main/CODE_OF_CONDUCT.md) applies to all UtilityJS community channels. 14 | 15 | ## Authors 16 | 17 | - Mostafa Shamsitabar ([Twitter](https://twitter.com/mimshins) | [Website](https://mimsh.in)) 18 | -------------------------------------------------------------------------------- /UtilityJs-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimshins/utilityjs/310ad1d404711faf578f6ab4a0cfd2fd96a77b83/UtilityJs-wide.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "utilityjs", 3 | "version": "0.1.0", 4 | "main": "./cjs/index.js", 5 | "module": "./esm/index.js", 6 | "description": "A collection of useful utility Classes, Functions, React Hooks and Components.", 7 | "repository": "git@github.com:mimshins/utilityjs.git", 8 | "author": "mimshins ", 9 | "license": "MIT", 10 | "sideEffects": false, 11 | "keywords": [ 12 | "utils", 13 | "utility", 14 | "functions", 15 | "functional programming", 16 | "react" 17 | ], 18 | "scripts": { 19 | "clear": "rimraf dist", 20 | "build:npm": "node ./scripts/build-npm-module.js", 21 | "build:ts:esm": "tsc -p tsconfig.json", 22 | "build:ts:cjs": "tsc -p tsconfig.cjs.json", 23 | "build:ts": "npm-run-all build:ts:esm build:ts:cjs", 24 | "prebuild:ts": "npm run clear" 25 | }, 26 | "peerDependencies": { 27 | "react": ">=16.8", 28 | "react-dom": ">=16.8" 29 | }, 30 | "devDependencies": { 31 | "@types/fs-extra": "^9.0.12", 32 | "@types/lodash.debounce": "^4.0.6", 33 | "@types/lodash.throttle": "^4.1.6", 34 | "@types/node": "^16.7.13", 35 | "@types/react": "^18.0.21", 36 | "@types/react-dom": "^18.0.6", 37 | "@types/rimraf": "^3.0.2", 38 | "@typescript-eslint/eslint-plugin": "^4.31.0", 39 | "@typescript-eslint/parser": "^4.31.0", 40 | "eslint": "^7.32.0", 41 | "eslint-config-prettier": "^8.3.0", 42 | "eslint-import-resolver-alias": "^1.1.2", 43 | "eslint-plugin-import": "^2.24.2", 44 | "eslint-plugin-react": "^7.25.1", 45 | "eslint-plugin-react-hooks": "^4.2.0", 46 | "fast-glob": "^3.2.7", 47 | "fs-extra": "^10.0.0", 48 | "npm-run-all": "^4.1.5", 49 | "prettier": "^2.3.2", 50 | "rimraf": "^3.0.2", 51 | "typescript": "^4.4.2" 52 | }, 53 | "dependencies": { 54 | "lodash.debounce": "^4.0.8", 55 | "lodash.throttle": "^4.1.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /scripts/build-npm-module.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fse = require("fs-extra"); 3 | 4 | const moduleType = process.argv[2]; 5 | 6 | const args = process.argv.slice(3); 7 | 8 | const packagePath = process.cwd(); 9 | 10 | const buildPath = path.join(packagePath, "dist"); 11 | const srcPath = path.join(packagePath, "src"); 12 | 13 | const esmPath = path.join(buildPath, "esm", moduleType); 14 | const cjsPath = path.join(buildPath, "cjs", moduleType); 15 | const npmPath = path.join(buildPath, "npm"); 16 | 17 | const modules = args 18 | .map(arg => path.join(srcPath, moduleType, arg)) 19 | .filter(path => fse.existsSync(path)) 20 | .map(path => [path, path.replace(`${srcPath}/${moduleType}/`, "")]); 21 | 22 | const ensureOutDirs = async () => { 23 | await fse.ensureDir(npmPath); 24 | await Promise.all( 25 | modules.map(module => fse.ensureDir(path.join(npmPath, module[1]))) 26 | ); 27 | }; 28 | 29 | const createModules = async () => { 30 | await Promise.all( 31 | modules.map(async module => { 32 | await fse.copyFile( 33 | path.join(`${cjsPath}/${module[1]}`, `${module[1]}.js`), 34 | path.join(npmPath, module[1], `${module[1]}.js`) 35 | ); 36 | await fse.copyFile( 37 | path.join(`${cjsPath}/${module[1]}`, `index.js`), 38 | path.join(npmPath, module[1], `index.js`) 39 | ); 40 | await fse.copyFile( 41 | path.join(`${cjsPath}/${module[1]}`, `${module[1]}.d.ts`), 42 | path.join(npmPath, module[1], `${module[1]}.d.ts`) 43 | ); 44 | await fse.copyFile( 45 | path.join(`${cjsPath}/${module[1]}`, `index.d.ts`), 46 | path.join(npmPath, module[1], `index.d.ts`) 47 | ); 48 | await fse.ensureDir(`${npmPath}/${module[1]}/esm`); 49 | await fse.copyFile( 50 | path.join(`${esmPath}/${module[1]}`, `${module[1]}.js`), 51 | path.join(npmPath, module[1], "esm", `${module[1]}.js`) 52 | ); 53 | await fse.copyFile( 54 | path.join(`${esmPath}/${module[1]}`, `index.js`), 55 | path.join(npmPath, module[1], "esm", `index.js`) 56 | ); 57 | }) 58 | ); 59 | }; 60 | 61 | const copyFromModules = async fileName => { 62 | await Promise.all( 63 | modules.map(module => 64 | fse.copyFile( 65 | path.join(module[0], fileName), 66 | path.join(npmPath, module[1], fileName) 67 | ) 68 | ) 69 | ); 70 | }; 71 | 72 | const copyPackageJson = async () => { 73 | await copyFromModules("package.json"); 74 | }; 75 | 76 | const copyREADME = async () => { 77 | await copyFromModules("README.md"); 78 | }; 79 | 80 | (async () => { 81 | try { 82 | await ensureOutDirs(); 83 | await createModules(); 84 | await copyREADME(); 85 | await copyPackageJson(); 86 | } catch (err) { 87 | // eslint-disable-next-line no-console 88 | console.error(err); 89 | process.exit(1); 90 | } 91 | })(); 92 | -------------------------------------------------------------------------------- /src/data-structure/Comparator/Comparator.ts: -------------------------------------------------------------------------------- 1 | export type CompareFunction = (a: T, b: T) => -1 | 0 | 1; 2 | 3 | export default class Comparator { 4 | private compare: CompareFunction; 5 | 6 | static defaultComparatorFunction = (a: T, b: T): -1 | 0 | 1 => { 7 | if (a === b) return 0; 8 | return a < b ? -1 : 1; 9 | }; 10 | 11 | constructor(compareFunction?: CompareFunction) { 12 | this.compare = compareFunction || Comparator.defaultComparatorFunction; 13 | } 14 | 15 | public isEqual(a: T, b: T): boolean { 16 | return this.compare(a, b) === 0; 17 | } 18 | 19 | public isLessThan(a: T, b: T): boolean { 20 | return this.compare(a, b) === -1; 21 | } 22 | 23 | public isLessThanOrEqual(a: T, b: T): boolean { 24 | return this.isLessThan(a, b) || this.isEqual(a, b); 25 | } 26 | 27 | public isGreaterThan(a: T, b: T): boolean { 28 | return this.compare(a, b) === 1; 29 | } 30 | 31 | public isGreaterThanOrEqual(a: T, b: T): boolean { 32 | return this.isGreaterThan(a, b) || this.isEqual(a, b); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/data-structure/Comparator/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | Comparator 4 |

5 |
6 | 7 |
8 | 9 | A utility class that compares its comparable arguments. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/comparator?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/comparator) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/comparator?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/comparator) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/comparator?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/comparator) 15 | 16 | ```bash 17 | npm i @utilityjs/comparator | yarn add @utilityjs/comparator 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```ts 27 | interface Item { 28 | type: string; 29 | value: number; 30 | } 31 | 32 | const comparator = new Comparator((a, b) => { 33 | if (a.value === b.value) return 0; 34 | return a.value < b.value ? -1 : 1; 35 | }); 36 | 37 | // TRUE 38 | comparator.isEqual({ type: "a", value: 0}, { type: "b", value: 0}); 39 | ``` 40 | 41 | ## API 42 | 43 | ### `Comparator(compareFunction?)` 44 | 45 | ```ts 46 | declare type CompareFunction = (a: T, b: T) => -1 | 0 | 1; 47 | declare class Comparator { 48 | private compare; 49 | static defaultComparatorFunction: (a: U, b: U) => -1 | 0 | 1; 50 | constructor(compareFunction?: CompareFunction); 51 | isEqual(a: T, b: T): boolean; 52 | isLessThan(a: T, b: T): boolean; 53 | isLessThanOrEqual(a: T, b: T): boolean; 54 | isGreaterThan(a: T, b: T): boolean; 55 | isGreaterThanOrEqual(a: T, b: T): boolean; 56 | } 57 | ``` -------------------------------------------------------------------------------- /src/data-structure/Comparator/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Comparator"; 2 | export * from "./Comparator"; 3 | -------------------------------------------------------------------------------- /src/data-structure/Comparator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/comparator", 3 | "version": "1.0.1", 4 | "description": "A utility class that compares its comparable arguments.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/data-structure/Comparator" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/data-structure/Comparator", 16 | "keywords": [ 17 | "javascript", 18 | "typescript", 19 | "compare function", 20 | "comparator", 21 | "compare class", 22 | "class", 23 | "comparator class" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/data-structure/DisjointSet/DisjointSet.ts: -------------------------------------------------------------------------------- 1 | type KeyGenerator = (value: T) => string; 2 | 3 | const _defaultKeyGenerator = (value: T) => JSON.stringify(value); 4 | 5 | export class Item { 6 | private _value: T; 7 | 8 | private _parent: Item | null = null; 9 | private _children: Record> = {}; 10 | 11 | private _keyGenerator: KeyGenerator; 12 | 13 | constructor(value: T, keyGenerator: KeyGenerator = _defaultKeyGenerator) { 14 | this._value = value; 15 | this._keyGenerator = keyGenerator; 16 | } 17 | 18 | public getKey(): string { 19 | return this._keyGenerator(this._value); 20 | } 21 | 22 | public getRoot(): Item { 23 | return this.isRoot() ? this : (this._parent as Item).getRoot(); 24 | } 25 | 26 | public isRoot(): boolean { 27 | return this._parent === null; 28 | } 29 | 30 | public getRank(): number { 31 | return this.getChildren().reduce( 32 | (result, child) => result + 1 + child.getRank(), 33 | 0 34 | ); 35 | } 36 | 37 | public getChildren(): Item[] { 38 | return Object.keys(this._children).map(key => this._children[key]); 39 | } 40 | 41 | public setParent(parent: Item, alsoAsParentChild = false): void { 42 | this._parent = parent; 43 | if (alsoAsParentChild) parent.addChild(this); 44 | } 45 | 46 | public getParent(): Item | null { 47 | return this._parent; 48 | } 49 | 50 | public addChild(child: Item): void { 51 | this._children[child.getKey()] = child; 52 | child.setParent(this); 53 | } 54 | } 55 | 56 | export default class DisjointSet { 57 | private _items: Record> = {}; 58 | 59 | private _keyGenerator: KeyGenerator; 60 | 61 | constructor(keyGenerator: KeyGenerator = _defaultKeyGenerator) { 62 | this._keyGenerator = keyGenerator; 63 | } 64 | 65 | public makeSet(value: T): void { 66 | const key = this._keyGenerator(value); 67 | const item = this._items[key]; 68 | 69 | if (!item) this._items[key] = new Item(value, this._keyGenerator); 70 | } 71 | 72 | public find(value: T): string | null { 73 | const item = this._items[this._keyGenerator(value)]; 74 | 75 | if (!item) return null; 76 | return item.getRoot().getKey(); 77 | } 78 | 79 | public union(valueA: T, valueB: T): DisjointSet { 80 | const rootKeyA = this.find(valueA); 81 | const rootKeyB = this.find(valueB); 82 | 83 | if (rootKeyA === null) 84 | throw new Error(`${String(valueA)} isn't in any set.`); 85 | if (rootKeyB === null) 86 | throw new Error(`${String(valueB)} isn't in any set.`); 87 | 88 | if (rootKeyA === rootKeyB) return this; 89 | 90 | const rootA = this._items[rootKeyA]; 91 | const rootB = this._items[rootKeyB]; 92 | 93 | if (rootA.getRank() < rootB.getRank()) { 94 | rootB.addChild(rootA); 95 | return this; 96 | } 97 | 98 | rootA.addChild(rootB); 99 | return this; 100 | } 101 | 102 | public inSameSet(valueA: T, valueB: T): boolean { 103 | const rootKeyA = this.find(valueA); 104 | const rootKeyB = this.find(valueB); 105 | 106 | if (rootKeyA === null) 107 | throw new Error(`${String(valueA)} isn't in any set.`); 108 | if (rootKeyB === null) 109 | throw new Error(`${String(valueB)} isn't in any set.`); 110 | 111 | return rootKeyA === rootKeyB; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/data-structure/DisjointSet/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | DisjointSet 4 |

5 |
6 | 7 |
8 | 9 | An implementation of DisjointSet data structure. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/disjoint-set?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/disjoint-set) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/disjoint-set?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/disjoint-set) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/disjoint-set?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/disjoint-set) 15 | 16 | ```bash 17 | npm i @utilityjs/disjoint-set | yarn add @utilityjs/disjoint-set 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ```ts 25 | declare type KeyGenerator = (value: T) => string; 26 | 27 | export declare class Item { 28 | constructor(value: T, keyGenerator?: KeyGenerator); 29 | getKey(): string; 30 | getRoot(): Item; 31 | isRoot(): boolean; 32 | getRank(): number; 33 | getChildren(): Item[]; 34 | setParent(parent: Item, alsoAsParentChild?: boolean): void; 35 | getParent(): Item | null; 36 | addChild(child: Item): void; 37 | } 38 | 39 | export default class DisjointSet { 40 | constructor(keyGenerator?: KeyGenerator); 41 | makeSet(value: T): void; 42 | find(value: T): string | null; 43 | union(valueA: T, valueB: T): DisjointSet; 44 | inSameSet(valueA: T, valueB: T): boolean; 45 | } 46 | ``` -------------------------------------------------------------------------------- /src/data-structure/DisjointSet/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./DisjointSet"; 2 | export * from "./DisjointSet"; 3 | -------------------------------------------------------------------------------- /src/data-structure/DisjointSet/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/disjoint-set", 3 | "version": "1.0.0", 4 | "description": "An implementation of DisjointSet data structure.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/data-structure/DisjointSet" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/data-structure/DisjointSet", 16 | "keywords": [ 17 | "javascript", 18 | "typescript", 19 | "data structure", 20 | "algorithm", 21 | "disjoint set", 22 | "union-find", 23 | "merge-find" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/data-structure/Graph/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | Graph 4 |

5 |
6 | 7 |
8 | 9 | An implementation of Graph data structure. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/graph?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/graph) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/graph?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/graph) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/graph?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/graph) 15 | 16 | ```bash 17 | npm i @utilityjs/graph | yarn add @utilityjs/graph 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ### `Graph(isDirected?)` 25 | 26 | ```ts 27 | interface SearchCallbacks { 28 | onEnter: (previous: Vertex | null, current: Vertex) => void; 29 | onLeave: (previous: Vertex | null, current: Vertex) => void; 30 | shouldTraverse: ( 31 | previous: Vertex | null, 32 | current: Vertex, 33 | next: Vertex 34 | ) => boolean; 35 | } 36 | 37 | export declare class Vertex { 38 | constructor(value: T, key?: string | null); 39 | getValue(): T; 40 | setValue(value: T): void; 41 | addEdge(edge: Edge): void; 42 | deleteEdge(edge: Edge): void; 43 | getKey(): string; 44 | getEdges(): Edge[]; 45 | getDegree(): number; 46 | getNeighborEdge(vertex: Vertex): Edge | null; 47 | hasEdge(edge: Edge): boolean; 48 | getSelfLoop(): Edge | null; 49 | hasSelfLoop(): boolean; 50 | hasNeighbor(vertex: Vertex): boolean; 51 | getNeighbors(): Vertex[]; 52 | clearEdges(): void; 53 | } 54 | 55 | export declare class Edge { 56 | constructor( 57 | vA: Vertex, 58 | vB: Vertex, 59 | weight?: number, 60 | key?: string | null 61 | ); 62 | setVA(vA: Vertex): void; 63 | setVB(vB: Vertex): void; 64 | getVA(): Vertex; 65 | getVB(): Vertex; 66 | isSelfLoop(): boolean; 67 | setWeight(weight: number): void; 68 | getWeight(): number; 69 | getKey(): string; 70 | reverse(): void; 71 | } 72 | 73 | export default class Graph { 74 | constructor(isDirected?: boolean); 75 | isDirected(): boolean; 76 | getVertex(key: string): Vertex | null; 77 | addVertex(vertex: Vertex): void; 78 | getVertices(): Vertex[]; 79 | getEdges(): Edge[]; 80 | getWeight(): number; 81 | getVerticesIndexMap(): Record; 82 | reverse(): void; 83 | addEdge(edge: Edge): void; 84 | findEdge(edge: Edge): Edge | null; 85 | findEdge(vA: Vertex, vB: Vertex): Edge | null; 86 | deleteEdge(edge: Edge): void; 87 | getAdjacencyMatrix(unweighted?: boolean): number[][]; 88 | breadthFirstSearch( 89 | startVertex: Vertex, 90 | callbacks?: SearchCallbacks 91 | ): void; 92 | depthFirstSearch( 93 | startVertex: Vertex, 94 | callbacks?: SearchCallbacks 95 | ): void; 96 | } 97 | ``` -------------------------------------------------------------------------------- /src/data-structure/Graph/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Graph"; 2 | export * from "./Graph"; 3 | -------------------------------------------------------------------------------- /src/data-structure/Graph/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/graph", 3 | "version": "1.2.1", 4 | "description": "An implementation of Graph data structure.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/data-structure/Graph" 14 | }, 15 | "dependencies": { 16 | "@utilityjs/linked-list": "^1.0.2" 17 | }, 18 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/data-structure/Graph", 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "data structure", 23 | "algorithm", 24 | "graph", 25 | "weighted graph", 26 | "unweighted graph", 27 | "directed graph", 28 | "undirected graph" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/data-structure/Heap/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | Heap 4 |

5 |
6 | 7 |
8 | 9 | An implementation of abstract Heap class. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/heap?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/heap) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/heap?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/heap) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/heap?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/heap) 15 | 16 | ```bash 17 | npm i @utilityjs/heap | yarn add @utilityjs/heap 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | This is an abstract class with a single abstract method (`pairIsInCorrectOrder`).\ 27 | This method checks if pair of heap elements is in correct order. 28 | 29 | ## API 30 | 31 | ### `Heap(compareFunction?)` 32 | 33 | ```ts 34 | abstract class Heap { 35 | protected comparator: Comparator; 36 | constructor(compareFunction?: CompareFunction); 37 | abstract pairIsInCorrectOrder( 38 | firstItem: T | null, 39 | secondItem: T | null 40 | ): boolean; 41 | getLeftChildIndex(parentIndex: number): number; 42 | getRightChildIndex(parentIndex: number): number; 43 | getParentIndex(childIndex: number): number; 44 | hasParent(childIndex: number): boolean; 45 | hasLeftChild(parentIndex: number): boolean; 46 | hasRightChild(parentIndex: number): boolean; 47 | getLeftChild(parentIndex: number): T | null; 48 | getRightChild(parentIndex: number): T | null; 49 | getParent(childIndex: number): T | null; 50 | isEmpty(): boolean; 51 | toString(): string; 52 | peek(): T | null; 53 | poll(): T | null; 54 | add(item: T): void; 55 | remove(item: T): void; 56 | find(item: T): number[]; 57 | swap(index1: number, index2: number): void; 58 | heapifyDown(startIndex?: number): void; 59 | heapifyUp(startIndex?: number): void; 60 | } 61 | ``` -------------------------------------------------------------------------------- /src/data-structure/Heap/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Heap"; 2 | -------------------------------------------------------------------------------- /src/data-structure/Heap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/heap", 3 | "version": "1.0.1", 4 | "description": "An implementation of abstract Heap class.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/data-structure/Heap" 14 | }, 15 | "dependencies": { 16 | "@utilityjs/comparator": "^1.0.1" 17 | }, 18 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/data-structure/Heap", 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "data structure", 23 | "algorithm", 24 | "heap" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/data-structure/LinkedList/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | LinkedList 4 |

5 |
6 | 7 |
8 | 9 | An implementation of LinkedList data structure. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/linked-list?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/linked-list) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/linked-list?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/linked-list) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/linked-list?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/linked-list) 15 | 16 | ```bash 17 | npm i @utilityjs/linked-list | yarn add @utilityjs/linked-list 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```ts 27 | interface Item { 28 | type: string; 29 | value: number; 30 | } 31 | 32 | const compareListItems = (a: Item, b: Item) => { 33 | if (a.value === b.value) return 0; 34 | return a.value < b.value ? -1 : 1; 35 | }; 36 | 37 | const list = new LinkedList(compareListItems); 38 | 39 | list.append({ type: "b", value: 1 }); // b -> null 40 | list.prepend({ type: "a", value: 0 }); // a -> b -> null 41 | list.fromArray([ 42 | { type: "c", value: 2 }, 43 | { type: "d", value: 3 }, 44 | { type: "f", value: 4 }, 45 | ]); // a -> b -> c -> d -> f -> null 46 | ``` 47 | 48 | ## API 49 | 50 | ### `LinkedList(compareFunction?)` 51 | 52 | ```ts 53 | export declare class Node { 54 | constructor(value: T, next?: Node | null); 55 | getValue(): T; 56 | setValue(value: T): void; 57 | getNext(): Node | null; 58 | setNext(next: Node | null): void; 59 | hasNext(): boolean; 60 | } 61 | 62 | export default class LinkedList { 63 | constructor(compareFunction?: CompareFunction); 64 | getHead(): Node | null; 65 | getTail(): Node | null; 66 | isEmpty(): boolean; 67 | getLength(): number; 68 | append(value: T): void; 69 | prepend(value: T): void; 70 | traverse(callback: (node: Node, index: number) => void | boolean): void; 71 | deleteHead(): void; 72 | deleteTail(): void; 73 | delete(value: T): void; 74 | reverse(): void; 75 | fromArray(array: T[]): void; 76 | toArray(): T[]; 77 | } 78 | ``` -------------------------------------------------------------------------------- /src/data-structure/LinkedList/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./LinkedList"; 2 | export * from "./LinkedList"; 3 | -------------------------------------------------------------------------------- /src/data-structure/LinkedList/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/linked-list", 3 | "version": "1.0.2", 4 | "description": "An implementation of LinkedList data structure.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/data-structure/LinkedList" 14 | }, 15 | "dependencies": { 16 | "@utilityjs/comparator": "^1.0.1" 17 | }, 18 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/data-structure/LinkedList", 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "data structure", 23 | "algorithm", 24 | "linked list", 25 | "list" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/data-structure/MaxHeap/MaxHeap.ts: -------------------------------------------------------------------------------- 1 | import Heap from "@utilityjs/heap"; 2 | 3 | export default class MaxHeap extends Heap { 4 | pairIsInCorrectOrder(firstItem: T | null, secondItem: T | null): boolean { 5 | if (firstItem == null || secondItem == null) return false; 6 | 7 | return this.comparator.isGreaterThanOrEqual(firstItem, secondItem); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/data-structure/MaxHeap/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | MaxHeap 4 |

5 |
6 | 7 |
8 | 9 | An implementation of MaxHeap data structure. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/max-heap?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/max-heap) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/max-heap?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/max-heap) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/max-heap?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/max-heap) 15 | 16 | ```bash 17 | npm i @utilityjs/max-heap | yarn add @utilityjs/max-heap 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ### `MaxHeap(compareFunction?)` 25 | 26 | ```ts 27 | class MaxHeap { 28 | constructor(compareFunction?: CompareFunction); 29 | pairIsInCorrectOrder(firstItem: T | null, secondItem: T | null): boolean; 30 | getLeftChildIndex(parentIndex: number): number; 31 | getRightChildIndex(parentIndex: number): number; 32 | getParentIndex(childIndex: number): number; 33 | hasParent(childIndex: number): boolean; 34 | hasLeftChild(parentIndex: number): boolean; 35 | hasRightChild(parentIndex: number): boolean; 36 | getLeftChild(parentIndex: number): T | null; 37 | getRightChild(parentIndex: number): T | null; 38 | getParent(childIndex: number): T | null; 39 | isEmpty(): boolean; 40 | toString(): string; 41 | peek(): T | null; 42 | poll(): T | null; 43 | add(item: T): void; 44 | remove(item: T): void; 45 | find(item: T): number[]; 46 | swap(index1: number, index2: number): void; 47 | heapifyDown(startIndex?: number): void; 48 | heapifyUp(startIndex?: number): void; 49 | } 50 | ``` -------------------------------------------------------------------------------- /src/data-structure/MaxHeap/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./MaxHeap"; 2 | -------------------------------------------------------------------------------- /src/data-structure/MaxHeap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/max-heap", 3 | "version": "1.0.1", 4 | "description": "An implementation of MaxHeap data structure.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/data-structure/MaxHeap" 14 | }, 15 | "dependencies": { 16 | "@utilityjs/heap": "^1.0.1" 17 | }, 18 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/data-structure/MaxHeap", 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "data structure", 23 | "algorithm", 24 | "heap", 25 | "max heap", 26 | "maximum heap" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/data-structure/MinHeap/MinHeap.ts: -------------------------------------------------------------------------------- 1 | import Heap from "@utilityjs/heap"; 2 | 3 | export default class MinHeap extends Heap { 4 | pairIsInCorrectOrder(firstItem: T | null, secondItem: T | null): boolean { 5 | if (firstItem == null || secondItem == null) return false; 6 | 7 | return this.comparator.isLessThanOrEqual(firstItem, secondItem); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/data-structure/MinHeap/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | MinHeap 4 |

5 |
6 | 7 |
8 | 9 | An implementation of MinHeap data structure. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/min-heap?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/min-heap) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/min-heap?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/min-heap) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/min-heap?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/min-heap) 15 | 16 | ```bash 17 | npm i @utilityjs/min-heap | yarn add @utilityjs/min-heap 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ### `MinHeap(compareFunction?)` 25 | 26 | ```ts 27 | class MinHeap { 28 | constructor(compareFunction?: CompareFunction); 29 | pairIsInCorrectOrder(firstItem: T | null, secondItem: T | null): boolean; 30 | getLeftChildIndex(parentIndex: number): number; 31 | getRightChildIndex(parentIndex: number): number; 32 | getParentIndex(childIndex: number): number; 33 | hasParent(childIndex: number): boolean; 34 | hasLeftChild(parentIndex: number): boolean; 35 | hasRightChild(parentIndex: number): boolean; 36 | getLeftChild(parentIndex: number): T | null; 37 | getRightChild(parentIndex: number): T | null; 38 | getParent(childIndex: number): T | null; 39 | isEmpty(): boolean; 40 | toString(): string; 41 | peek(): T | null; 42 | poll(): T | null; 43 | add(item: T): void; 44 | remove(item: T): void; 45 | find(item: T): number[]; 46 | swap(index1: number, index2: number): void; 47 | heapifyDown(startIndex?: number): void; 48 | heapifyUp(startIndex?: number): void; 49 | } 50 | ``` -------------------------------------------------------------------------------- /src/data-structure/MinHeap/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./MinHeap"; 2 | -------------------------------------------------------------------------------- /src/data-structure/MinHeap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/min-heap", 3 | "version": "1.0.1", 4 | "description": "An implementation of MinHeap data structure.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/data-structure/MinHeap" 14 | }, 15 | "dependencies": { 16 | "@utilityjs/heap": "^1.0.1" 17 | }, 18 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/data-structure/MinHeap", 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "data structure", 23 | "algorithm", 24 | "heap", 25 | "min heap", 26 | "minimum heap" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/data-structure/PriorityQueue/PriorityQueue.ts: -------------------------------------------------------------------------------- 1 | import Comparator, { CompareFunction } from "@utilityjs/comparator"; 2 | import MinHeap from "@utilityjs/min-heap"; 3 | 4 | export default class PriorityQueue extends MinHeap { 5 | private priorities: Map; 6 | 7 | private valueComparator: Comparator; 8 | 9 | constructor(compareFunction?: CompareFunction) { 10 | // eslint-disable-next-line 11 | super(); 12 | 13 | this.priorities = new Map(); 14 | this.valueComparator = new Comparator(compareFunction); 15 | 16 | this.comparator = new Comparator((a: T, b: T) => { 17 | const pA = this.priorities.get(a); 18 | const pB = this.priorities.get(b); 19 | 20 | if (typeof pA !== "number" || typeof pB !== "number") 21 | throw new ReferenceError(); 22 | 23 | if (pA === pB) return 0; 24 | return pA < pB ? -1 : 1; 25 | }); 26 | } 27 | 28 | override add(item: T, priority = 0): void { 29 | this.priorities.set(item, priority); 30 | // eslint-disable-next-line 31 | super.add(item); 32 | } 33 | 34 | override remove(item: T, comparator?: Comparator): void { 35 | // eslint-disable-next-line 36 | super.remove(item, comparator ?? this.valueComparator); 37 | this.priorities.delete(item); 38 | } 39 | 40 | changePriority(item: T, priority: number): void { 41 | this.remove(item); 42 | this.add(item, priority); 43 | } 44 | 45 | findByValue(item: T): number[] { 46 | // eslint-disable-next-line 47 | return this.find(item, this.valueComparator); 48 | } 49 | 50 | hasValue(item: T): boolean { 51 | return this.findByValue(item).length > 0; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/data-structure/PriorityQueue/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | PriorityQueue 4 |

5 |
6 | 7 |
8 | 9 | An implementation of PriorityQueue data structure. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/priority-queue?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/priority-queue) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/priority-queue?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/priority-queue) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/priority-queue?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/priority-queue) 15 | 16 | ```bash 17 | npm i @utilityjs/priority-queue | yarn add @utilityjs/priority-queue 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ### `PriorityQueue(compareFunction?)` 25 | 26 | ```ts 27 | export default class Vector { 28 | constructor(x: number, y: number); 29 | constructor(x: number, y: number, z: number); 30 | setX(x: number): Vector; 31 | getX(): number; 32 | setY(y: number): Vector; 33 | getY(): number; 34 | setZ(z: number): Vector; 35 | getZ(): number; 36 | setAxes(x: number, y: number): Vector; 37 | setAxes(x: number, y: number, z: number): Vector; 38 | add(vector: Vector): Vector; 39 | subtract(vector: Vector): Vector; 40 | multiply(vector: Vector): Vector; 41 | multiply(scalar: number): Vector; 42 | dotProduct(vector: Vector): number; 43 | crossProduct(vector: Vector): Vector; 44 | distance(vector: Vector): number; 45 | angleBetween(vector: Vector): number; 46 | lerp(vector: Vector, t: number): Vector; 47 | normalize(): Vector; 48 | reflect(surfaceNormal: Vector): Vector; 49 | reverse(): Vector; 50 | setMagnitude(magnitude: number): Vector; 51 | magnitude(): number; 52 | squaredMagnitude(): number; 53 | equalsTo(vector: Vector): boolean; 54 | clone(): Vector; 55 | toString(): string; 56 | toArray(): [number, number, number]; 57 | toObject(): { x: number; y: number; z: number; }; 58 | static lerp(vector1: Vector, vector2: Vector, t: number): Vector; 59 | static add(vector1: Vector, vector2: Vector): Vector; 60 | static subtract(vector1: Vector, vector2: Vector): Vector; 61 | static multiply(vector1: Vector, vector2: Vector): Vector; 62 | static multiply(vector1: Vector, scalar: number): Vector; 63 | static dotProduce(vector1: Vector, vector2: Vector): number; 64 | static crossProduct(vector1: Vector, vector2: Vector): Vector; 65 | static distance(vector1: Vector, vector2: Vector): number; 66 | static fromAngle(angleInRadians: number, magnitude?: number): Vector; 67 | static fromArray( 68 | arrayOfComponents: [number, number, number] | [number, number] 69 | ): Vector; 70 | } 71 | ``` -------------------------------------------------------------------------------- /src/data-structure/PriorityQueue/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./PriorityQueue"; 2 | -------------------------------------------------------------------------------- /src/data-structure/PriorityQueue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/priority-queue", 3 | "version": "1.0.0", 4 | "description": "An implementation of PriorityQueue data structure.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/data-structure/PriorityQueue" 14 | }, 15 | "dependencies": { 16 | "@utilityjs/comparator": "^1.0.1", 17 | "@utilityjs/min-heap": "^1.0.1" 18 | }, 19 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/data-structure/PriorityQueue", 20 | "keywords": [ 21 | "javascript", 22 | "typescript", 23 | "data structure", 24 | "algorithm", 25 | "queue", 26 | "priority queue", 27 | "PriorityQueue" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/data-structure/Queue/Queue.ts: -------------------------------------------------------------------------------- 1 | import LinkedList from "@utilityjs/linked-list"; 2 | 3 | export default class Queue { 4 | private list: LinkedList; 5 | 6 | constructor() { 7 | this.list = new LinkedList(); 8 | } 9 | 10 | isEmpty(): boolean { 11 | return this.list.isEmpty(); 12 | } 13 | 14 | enqueue(value: T): void { 15 | this.list.append(value); 16 | } 17 | 18 | dequeue(): T | null { 19 | const dequeued = this.peek(); 20 | 21 | this.list.deleteHead(); 22 | 23 | return dequeued; 24 | } 25 | 26 | peek(): T | null { 27 | const _head = this.list.getHead(); 28 | 29 | if (!_head) return null; 30 | 31 | return _head.getValue(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/data-structure/Queue/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | Queue 4 |

5 |
6 | 7 |
8 | 9 | An implementation of Queue data structure. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/queue?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/queue) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/queue?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/queue) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/queue?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/queue) 15 | 16 | ```bash 17 | npm i @utilityjs/queue | yarn add @utilityjs/queue 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ### `Queue()` 25 | 26 | ```ts 27 | export default class Queue { 28 | constructor(); 29 | isEmpty(): boolean; 30 | enqueue(value: T): void; 31 | dequeue(): T | null; 32 | peek(): T | null; 33 | } 34 | ``` -------------------------------------------------------------------------------- /src/data-structure/Queue/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Queue"; 2 | -------------------------------------------------------------------------------- /src/data-structure/Queue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/queue", 3 | "version": "1.0.1", 4 | "description": "An implementation of Queue data structure.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/data-structure/Queue" 14 | }, 15 | "dependencies": { 16 | "@utilityjs/linked-list": "^1.0.1" 17 | }, 18 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/data-structure/Queue", 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "data structure", 23 | "algorithm", 24 | "queue" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/data-structure/Stack/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | Stack 4 |

5 |
6 | 7 |
8 | 9 | An implementation of Stack data structure. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/stack?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/stack) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/stack?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/stack) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/stack?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/stack) 15 | 16 | ```bash 17 | npm i @utilityjs/stack | yarn add @utilityjs/stack 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ### `Stack()` 25 | 26 | ```ts 27 | export default class Stack { 28 | constructor(); 29 | isEmpty(): boolean; 30 | push(value: T): void; 31 | pop(): T | null; 32 | peek(): T | null; 33 | toArray(): T[]; 34 | } 35 | ``` -------------------------------------------------------------------------------- /src/data-structure/Stack/Stack.ts: -------------------------------------------------------------------------------- 1 | import LinkedList from "@utilityjs/linked-list"; 2 | 3 | export default class Stack { 4 | private list: LinkedList; 5 | 6 | constructor() { 7 | this.list = new LinkedList(); 8 | } 9 | 10 | isEmpty(): boolean { 11 | return this.list.isEmpty(); 12 | } 13 | 14 | push(value: T): void { 15 | this.list.prepend(value); 16 | } 17 | 18 | pop(): T | null { 19 | const poppedValue = this.peek(); 20 | 21 | this.list.deleteHead(); 22 | 23 | return poppedValue; 24 | } 25 | 26 | peek(): T | null { 27 | const _head = this.list.getHead(); 28 | 29 | if (!_head) return null; 30 | 31 | return _head.getValue(); 32 | } 33 | 34 | toArray(): T[] { 35 | return this.list.toArray(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/data-structure/Stack/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Stack"; 2 | -------------------------------------------------------------------------------- /src/data-structure/Stack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/stack", 3 | "version": "1.0.1", 4 | "description": "An implementation of Stack data structure.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/data-structure/Stack" 14 | }, 15 | "dependencies": { 16 | "@utilityjs/linked-list": "^1.0.1" 17 | }, 18 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/data-structure/Stack", 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "data structure", 23 | "algorithm", 24 | "stack" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/data-structure/Vector/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | Vector 4 |

5 |
6 | 7 |
8 | 9 | An implementation of a two or three-dimensional Vector. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/vector?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/vector) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/vector?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/vector) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/vector?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/vector) 15 | 16 | ```bash 17 | npm i @utilityjs/vector | yarn add @utilityjs/vector 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ### `Vector(x, y, z?)` 25 | 26 | ```ts 27 | export default class Vector { 28 | constructor(x: number, y: number); 29 | constructor(x: number, y: number, z: number); 30 | setX(x: number): Vector; 31 | getX(): number; 32 | setY(y: number): Vector; 33 | getY(): number; 34 | setZ(z: number): Vector; 35 | getZ(): number; 36 | setAxes(x: number, y: number): Vector; 37 | setAxes(x: number, y: number, z: number): Vector; 38 | add(vector: Vector): Vector; 39 | subtract(vector: Vector): Vector; 40 | multiply(vector: Vector): Vector; 41 | multiply(scalar: number): Vector; 42 | dotProduct(vector: Vector): number; 43 | crossProduct(vector: Vector): Vector; 44 | distance(vector: Vector): number; 45 | angleBetween(vector: Vector): number; 46 | lerp(vector: Vector, t: number): Vector; 47 | normalize(): Vector; 48 | getNormalizedVector(): Vector; 49 | reflect(surfaceNormal: Vector): Vector; 50 | reverse(): Vector; 51 | setMagnitude(magnitude: number): Vector; 52 | magnitude(): number; 53 | squaredMagnitude(): number; 54 | equalsTo(vector: Vector): boolean; 55 | clone(): Vector; 56 | toString(): string; 57 | toArray(): [number, number, number]; 58 | toObject(): { 59 | x: number; 60 | y: number; 61 | z: number; 62 | }; 63 | static fromAngle(angleInRadians: number, magnitude?: number): Vector; 64 | static fromArray( 65 | arrayOfComponents: [number, number, number] | [number, number] 66 | ): Vector; 67 | } 68 | ``` -------------------------------------------------------------------------------- /src/data-structure/Vector/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Vector"; 2 | -------------------------------------------------------------------------------- /src/data-structure/Vector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/vector", 3 | "version": "1.0.0", 4 | "description": "An implementation of a two or three-dimensional Vector.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/data-structure/Vector" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/data-structure/Vector", 16 | "keywords": [ 17 | "javascript", 18 | "typescript", 19 | "data structure", 20 | "algorithm", 21 | "math", 22 | "direction", 23 | "position", 24 | "vector" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/function/createStoreContext/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | createStoreContext 4 |

5 |
6 | 7 |
8 | 9 | A React store-context that manages states of a tree where unnecessary re-renders have been omitted thanks to the Pub/Sub design pattern. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/create-store-context?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/create-store-context) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/create-store-context?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/create-store-context) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/create-store-context?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/create-store-context) 15 | 16 | ```bash 17 | npm i @utilityjs/create-store-context | yarn add @utilityjs/create-store-context 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## How does it work? 25 | 26 | The `` works as a Publisher and each `useStore` is a Subscriber. So when the state changes, only necessary Subscribers will be re-rendered. Since we are using `useRef` instead of `useState` the Publisher itself won't re-render. 27 | 28 | This way we guarantee that only the necessary components need to be re-rendered. 29 | 30 |
31 | 32 | ## Usage 33 | 34 | ```jsx 35 | import createStoreContext from "@utilityjs/create-store-context"; 36 | 37 | interface StoreState { 38 | count: number; 39 | log: () => void; 40 | increase: (amount: number) => void; 41 | } 42 | 43 | const { useStore, StoreProvider } = createStoreContext( 44 | (setState, getState) => ({ 45 | count: 0, 46 | log: () => void getState().count, 47 | increase: amount => 48 | setState(state => ({ ...state, count: state.count + amount })) 49 | }) 50 | ); 51 | 52 | const Controls = () => { 53 | const increase = useStore(state => state.increase); 54 | 55 | return ( 56 |
57 | 58 | 59 | 60 |
61 | ); 62 | }; 63 | 64 | const Display = () => { 65 | const { count, log } = useStore(state => ({ 66 | count: state.count, 67 | log: state.log 68 | })); 69 | 70 | log(); 71 | 72 | return
Count: {count}
; 73 | }; 74 | 75 | const Container = () => ( 76 |
77 |

Container

78 | 79 | 80 |
81 | ); 82 | 83 | const App = () => { 84 | return ( 85 |
86 |
87 | 88 |

App

89 | 90 |
91 |
92 |
93 | ); 94 | }; 95 | ``` 96 | 97 | ## API 98 | 99 | ### `createStoreContext(stateFactory)` 100 | 101 | ```ts 102 | type StateSelector = ( 103 | store: State 104 | ) => PartialState; 105 | 106 | type UseStoreHook = ( 107 | selector: StateSelector 108 | ) => PartialState; 109 | 110 | type StateFactory = ( 111 | setState: (setter: (prevState: S) => S) => void, 112 | getState: () => S 113 | ) => S; 114 | 115 | declare const createStoreContext: (stateFactory: StateFactory) => { 116 | StoreProvider: (props: { children: React.ReactNode }) => JSX.Element; 117 | useStore: UseStoreHook; 118 | }; 119 | ``` 120 | 121 | #### `stateFactory` 122 | 123 | An initialization function to initialize the states. 124 | -------------------------------------------------------------------------------- /src/function/createStoreContext/createStoreContext.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type SubscribeCallback = () => void; 4 | 5 | type StateSelector = (store: State) => PartialState; 6 | 7 | type UseStoreHook = ( 8 | selector: StateSelector 9 | ) => PartialState; 10 | 11 | type StateFactory = ( 12 | setState: (setter: (prevState: S) => S) => void, 13 | getState: () => S 14 | ) => S; 15 | 16 | const useSyncStore = ( 17 | subscribe: (onStoreChange: SubscribeCallback) => () => void, 18 | getSnapshot: () => State, 19 | getServerSnapshot?: () => State 20 | ): State => { 21 | const getIsomorphicSnapshot = 22 | typeof document !== "undefined" 23 | ? getSnapshot 24 | : getServerSnapshot ?? getSnapshot; 25 | 26 | const [state, setState] = React.useState(getIsomorphicSnapshot); 27 | 28 | const onStoreChange = React.useCallback( 29 | () => setState(() => getIsomorphicSnapshot()), 30 | [getIsomorphicSnapshot] 31 | ); 32 | 33 | // eslint-disable-next-line react-hooks/exhaustive-deps 34 | React.useEffect(() => subscribe(onStoreChange), [onStoreChange]); 35 | 36 | return state; 37 | }; 38 | 39 | const __STORE_SENTINEL__ = {}; 40 | 41 | const createStoreContext = ( 42 | stateFactory: StateFactory 43 | ): { 44 | StoreProvider: (props: { children: React.ReactNode }) => JSX.Element; 45 | useStore: UseStoreHook; 46 | } => { 47 | const StoreContext = React.createContext<{ 48 | getState: () => S; 49 | subscribe: (onStoreChange: SubscribeCallback) => () => void; 50 | } | null>(null); 51 | 52 | if (process.env.NODE_ENV !== "production") 53 | StoreContext.displayName = "StoreContext"; 54 | 55 | const StoreProvider = (props: { children: React.ReactNode }) => { 56 | const store = React.useRef(__STORE_SENTINEL__ as S); 57 | const subscribers = React.useRef(new Set()); 58 | 59 | const getState = React.useCallback(() => store.current, []); 60 | 61 | const setState = React.useCallback((setter: (prevState: S) => S) => { 62 | const newState = setter(store.current); 63 | 64 | store.current = newState; 65 | subscribers.current.forEach(cb => cb()); 66 | }, []); 67 | 68 | // Lazy initialization 69 | if (store.current === __STORE_SENTINEL__) 70 | store.current = stateFactory(setState, getState); 71 | 72 | const subscribe = React.useCallback((onStoreChange: SubscribeCallback) => { 73 | subscribers.current.add(onStoreChange); 74 | 75 | return () => { 76 | subscribers.current.delete(onStoreChange); 77 | }; 78 | }, []); 79 | 80 | const context = React.useMemo( 81 | () => ({ getState, subscribe }), 82 | [getState, subscribe] 83 | ); 84 | 85 | return ( 86 | 87 | {props.children} 88 | 89 | ); 90 | }; 91 | 92 | const useStore: UseStoreHook = selector => { 93 | const storeContext = React.useContext(StoreContext); 94 | 95 | if (!storeContext) { 96 | throw new Error( 97 | "[@utilityjs/create-store-context]: You can only use `useStore` in a subtree of ``." 98 | ); 99 | } 100 | 101 | const { getState, subscribe } = storeContext; 102 | 103 | const getSnapshot = React.useCallback( 104 | () => selector(getState()), 105 | // eslint-disable-next-line react-hooks/exhaustive-deps 106 | [selector] 107 | ); 108 | 109 | return useSyncStore(subscribe, getSnapshot, getSnapshot); 110 | }; 111 | 112 | return { useStore, StoreProvider }; 113 | }; 114 | 115 | export default createStoreContext; 116 | -------------------------------------------------------------------------------- /src/function/createStoreContext/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./createStoreContext"; 2 | -------------------------------------------------------------------------------- /src/function/createStoreContext/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/create-store-context", 3 | "version": "1.1.0", 4 | "description": "A React store-context that manages states of a tree where unnecessary re-renders have been omitted thanks to the Pub/Sub design pattern.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/function/createStoreContext" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/function/createStoreContext", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "context", 24 | "store", 25 | "state store", 26 | "state management", 27 | "context state" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/hook/useControlledProp/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useControlledProp 4 |

5 |
6 | 7 |
8 | 9 | A React hook that handles controllable props. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-controlled-prop?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-controlled-prop) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-controlled-prop?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-controlled-prop) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-controlled-prop?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-controlled-prop) 15 | 16 | ```bash 17 | npm i @utilityjs/use-controlled-prop | yarn add @utilityjs/use-controlled-prop 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```jsx 27 | import * as React from "react"; 28 | import useControlledProp from "@utilityjs/use-controlled-prop"; 29 | 30 | const MyComponent = (props, ref) => { 31 | const { 32 | onChange, 33 | value: valueProp, 34 | defaultValue: defaultValueProp, s 35 | ...otherProps 36 | } = props; 37 | 38 | const [value, setUncontrolledValue] = useControlledProp( 39 | valueProp, 40 | defaultValueProp, 41 | "" // Fallback value (if both values were `undefined`) 42 | ); 43 | 44 | return ( 45 | { 49 | if (onChange) onChange(e); 50 | // This line only works when the `valueProp` is not controlled 51 | setUncontrolledValue(e.target.value); 52 | }} 53 | {...otherProps} 54 | /> 55 | ); 56 | }; 57 | ``` 58 | 59 | ## API 60 | 61 | ### `useControlledProp(controlledValue, defaultValue, fallbackValue)` 62 | 63 | ```ts 64 | declare const useControlledProp: ( 65 | controlledValueProp: T | undefined, 66 | defaultValueProp: T | undefined, 67 | fallbackValue: T 68 | ) => [ 69 | value: T, 70 | setUncontrolledValue: React.Dispatch>, 71 | isControlled: boolean 72 | ]; 73 | ``` 74 | 75 | #### `controlledValueProp` 76 | 77 | The value to be controlled. 78 | 79 | #### `defaultValueProp` 80 | 81 | The default value. 82 | 83 | #### `fallbackValue` 84 | 85 | The value to fallback to when `controlledValue` and `defaultValueProp` were `undefined`. 86 | -------------------------------------------------------------------------------- /src/hook/useControlledProp/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useControlledProp"; 2 | -------------------------------------------------------------------------------- /src/hook/useControlledProp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-controlled-prop", 3 | "version": "1.1.2", 4 | "description": "A React hook that handles controllable props.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/useControlledProp" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useControlledProp", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "controlled", 25 | "uncontrolled", 26 | "prop", 27 | "use controlled", 28 | "use uncontrolled" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/hook/useControlledProp/useControlledProp.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const isEqual = (v1: T, v2: T): boolean => { 4 | if (typeof v1 !== typeof v2) return false; 5 | if (typeof v1 === "object") { 6 | if (!Array.isArray(v1)) return false; 7 | else if (v1.length !== (v2).length) return false; 8 | else { 9 | for (let i = 0; i < v1.length; i++) { 10 | if (v1[i] !== (v2)[i]) return false; 11 | } 12 | } 13 | } 14 | 15 | return true; 16 | }; 17 | 18 | const isUndef = (value: T): boolean => typeof value === "undefined"; 19 | 20 | const useControlledProp = ( 21 | controlledValueProp: T | undefined, 22 | defaultValueProp: T | undefined, 23 | fallbackValue: T 24 | ): [ 25 | value: T, 26 | setUncontrolledValue: React.Dispatch>, 27 | isControlled: boolean 28 | ] => { 29 | const { current: isControlled } = React.useRef(!isUndef(controlledValueProp)); 30 | const { current: defaultValue } = React.useRef(defaultValueProp); 31 | 32 | const { current: fallback } = React.useRef( 33 | isUndef(controlledValueProp) 34 | ? isUndef(defaultValueProp) 35 | ? fallbackValue 36 | : defaultValueProp 37 | : undefined 38 | ); 39 | 40 | if (process.env.NODE_ENV !== "production") { 41 | // eslint-disable-next-line react-hooks/rules-of-hooks 42 | React.useEffect(() => { 43 | if ( 44 | !isControlled && 45 | defaultValue !== defaultValueProp && 46 | !isEqual(defaultValue, defaultValueProp) 47 | ) { 48 | // eslint-disable-next-line no-console 49 | console.error( 50 | [ 51 | `[@utilityjs/use-controlled-prop]: A component is changing the defaultValue state of an uncontrolled prop after being initialized.`, 52 | `To suppress this warning use a controlled prop.` 53 | ].join(" ") 54 | ); 55 | } 56 | // eslint-disable-next-line react-hooks/exhaustive-deps 57 | }, [defaultValueProp]); 58 | 59 | // eslint-disable-next-line react-hooks/rules-of-hooks 60 | React.useEffect(() => { 61 | if (isControlled !== !isUndef(controlledValueProp)) { 62 | // eslint-disable-next-line no-console 63 | console.error( 64 | [ 65 | `[@utilityjs/use-controlled-prop]: A component is changing the ${ 66 | isControlled ? "" : "un" 67 | }controlled state of a prop to be ${ 68 | isControlled ? "un" : "" 69 | }controlled.`, 70 | "Decide between using a controlled or uncontrolled prop " + 71 | "for the lifetime of the component.", 72 | "The nature of the prop's state is determined during the first render, it's considered controlled if the prop is not `undefined`." 73 | ].join("\n") 74 | ); 75 | } 76 | // eslint-disable-next-line react-hooks/exhaustive-deps 77 | }, [controlledValueProp]); 78 | 79 | if ( 80 | isUndef(controlledValueProp) && 81 | isUndef(defaultValueProp) && 82 | isUndef(fallbackValue) 83 | ) { 84 | // eslint-disable-next-line no-console 85 | console.error( 86 | [ 87 | "[@utilityjs/use-controlled-prop]: The values you provide are `undefined`!", 88 | "To suppress this warning use a valid non-undefined controlled, default or fallback value." 89 | ].join(" ") 90 | ); 91 | } 92 | } 93 | 94 | const [uncontrolledValue, setUncontrolledValue] = React.useState(fallback); 95 | const value = isControlled ? controlledValueProp : uncontrolledValue; 96 | 97 | return [ 98 | value, 99 | React.useCallback((newValue: React.SetStateAction) => { 100 | if (!isControlled) setUncontrolledValue(newValue); 101 | // eslint-disable-next-line react-hooks/exhaustive-deps 102 | }, []), 103 | isControlled 104 | ]; 105 | }; 106 | 107 | export default useControlledProp; 108 | -------------------------------------------------------------------------------- /src/hook/useCopyToClipboard/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useCopyToClipboard 4 |

5 |
6 | 7 |
8 | 9 | A React hook for copying text to the clipboard. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-copy-to-clipboard?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-copy-to-clipboard) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-copy-to-clipboard?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-copy-to-clipboard) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-copy-to-clipboard?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-copy-to-clipboard) 15 | 16 | ```bash 17 | npm i @utilityjs/use-copy-to-clipboard | yarn add @utilityjs/use-copy-to-clipboard 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```tsx 27 | const App: React.FC = () => { 28 | const [isCopied, setIsCopied] = React.useState(false); 29 | 30 | const copy = useCopyToClipboard(); 31 | 32 | return ( 33 |
34 | 40 |
41 | ); 42 | }; 43 | ``` 44 | 45 | ## API 46 | 47 | ### `useCopyToClipboard()` 48 | 49 | ```ts 50 | declare const useCopyToClipboard: () => (text: string) => Promise; 51 | ``` 52 | -------------------------------------------------------------------------------- /src/hook/useCopyToClipboard/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useCopyToClipboard"; 2 | -------------------------------------------------------------------------------- /src/hook/useCopyToClipboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-copy-to-clipboard", 3 | "version": "1.0.1", 4 | "description": "A React hook for copying text to the clipboard.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/useCopyToClipboard" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useCopyToClipboard", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "clipboard", 25 | "copy text", 26 | "copy to clipboard" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/hook/useCopyToClipboard/useCopyToClipboard.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const copyText = (text: string): Promise => { 4 | // Uses the Async Clipboard API when available 5 | if (navigator.clipboard) return navigator.clipboard.writeText(text); 6 | 7 | // Fallback to old depricated API 8 | return (() => { 9 | const dummy = document.createElement("span"); 10 | dummy.textContent = text; 11 | dummy.style.whiteSpace = "pre"; 12 | 13 | document.body.appendChild(dummy); 14 | 15 | const selection = window.getSelection(); 16 | 17 | if (!selection) return Promise.reject(); 18 | 19 | const range = window.document.createRange(); 20 | 21 | selection.removeAllRanges(); 22 | range.selectNode(dummy); 23 | selection.addRange(range); 24 | 25 | try { 26 | window.document.execCommand("copy"); 27 | } catch (err) { 28 | return Promise.reject(); 29 | } 30 | 31 | selection.removeAllRanges(); 32 | document.body.removeChild(dummy); 33 | 34 | return Promise.resolve(); 35 | })(); 36 | }; 37 | 38 | const useCopyToClipboard = (): ((text: string) => Promise) => { 39 | return React.useCallback(async (text: string) => { 40 | try { 41 | await copyText(text); 42 | return true; 43 | } catch { 44 | if (navigator.clipboard) { 45 | // eslint-disable-next-line no-console 46 | console.error( 47 | "[@utilityjs/use-copy-to-clipboard]: The caller does not have permission to write to the clipboard!" 48 | ); 49 | } 50 | return false; 51 | } 52 | }, []); 53 | }; 54 | 55 | export default useCopyToClipboard; 56 | -------------------------------------------------------------------------------- /src/hook/useDarkMode/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useDarkMode 4 |

5 |
6 | 7 |
8 | 9 | A React hook that enables a SSR-friendly multi-tab persistent dark mode behaviour.\ 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-dark-mode?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-dark-mode) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-dark-mode?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-dark-mode) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-dark-mode?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-dark-mode) 15 | 16 | ```bash 17 | npm i @utilityjs/use-dark-mode | yarn add @utilityjs/use-dark-mode 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```tsx 27 | const App: React.FC = () => { 28 | const { isDarkMode, toggle } = useDarkMode(); 29 | 30 | return ( 31 |
32 | 33 |
{isDarkMode}
34 |
35 | ); 36 | }; 37 | ``` 38 | 39 | ## API 40 | 41 | ### `useDarkMode(options?)` 42 | 43 | ```ts 44 | interface Options { 45 | /** 46 | * The initial state of the dark mode.\ 47 | * If left unset, it will be set based on `(prefers-color-scheme: dark)` query. 48 | */ 49 | initialState?: boolean; 50 | /** 51 | * The key is used to persist the state. 52 | * 53 | * @default "utilityjs-dark-mode" 54 | */ 55 | storageKey?: string; 56 | /** 57 | * The class to toggle when state changes.\ 58 | * The specified class will be applied on dark mode. 59 | * 60 | * @default "dark-mode" 61 | */ 62 | toggleClassName?: string; 63 | /** 64 | * A function returning a storage.\ 65 | * The storage must fit `window.localStorage`'s api. 66 | * 67 | * @default () => localStorage 68 | */ 69 | getStorage?: () => Storage | null; 70 | } 71 | 72 | declare const useDarkMode: (options?: Options | undefined) => { 73 | isDarkMode: boolean; 74 | enable: () => void; 75 | disable: () => void; 76 | toggle: () => void; 77 | }; 78 | ``` 79 | 80 | #### `options` 81 | 82 | The options to adjust the hook. 83 | 84 | ##### `options.storageKey` - (`default: "utilityjs-dark-mode"`) 85 | 86 | The key is used to persist the state. 87 | 88 | ##### `options.initialState` 89 | 90 | The initial state of the dark mode.\ 91 | If left unset, it will be set based on `(prefers-color-scheme: dark)` query. 92 | 93 | ##### `options.getStorage` - (`default: () => localStorage`) 94 | 95 | A function returning a storage.\ 96 | The storage must fit `window.localStorage`'s api. 97 | 98 | ##### `options.toggleClassName` - (`default: "dark-mode"`) 99 | 100 | The class to toggle when state changes.\ 101 | The specified class will be applied on dark mode. -------------------------------------------------------------------------------- /src/hook/useDarkMode/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useDarkMode"; 2 | -------------------------------------------------------------------------------- /src/hook/useDarkMode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-dark-mode", 3 | "version": "1.0.1", 4 | "description": "A React hook that enables a SSR-friendly multi-tab persistent dark mode behaviour.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/useDarkMode" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useDarkMode", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "dependencies": { 20 | "@utilityjs/use-media-query": "^1.0.1", 21 | "@utilityjs/use-persisted-state": "^1.1.3" 22 | }, 23 | "keywords": [ 24 | "javascript", 25 | "typescript", 26 | "react", 27 | "react hook", 28 | "dark mode", 29 | "use dark mode", 30 | "use persistent dark mode" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /src/hook/useDarkMode/useDarkMode.ts: -------------------------------------------------------------------------------- 1 | import useMediaQuery from "@utilityjs/use-media-query"; 2 | import usePersistedState from "@utilityjs/use-persisted-state"; 3 | import * as React from "react"; 4 | 5 | interface Options { 6 | /** 7 | * The initial state of the dark mode.\ 8 | * If left unset, it will be set based on `(prefers-color-scheme: dark)` query. 9 | */ 10 | initialState?: boolean; 11 | /** 12 | * The key is used to persist the state. 13 | * 14 | * @default "utilityjs-dark-mode" 15 | */ 16 | storageKey?: string; 17 | /** 18 | * The class to toggle when state changes.\ 19 | * The specified class will be applied on dark mode. 20 | * 21 | * @default "dark-mode" 22 | */ 23 | toggleClassName?: string; 24 | /** 25 | * A function returning a storage. 26 | * The storage must fit `window.localStorage`'s api. 27 | * 28 | * @default () => localStorage 29 | */ 30 | getStorage?: () => Storage | null; 31 | } 32 | 33 | const addClassName = (element: HTMLElement, className: string) => 34 | void (element.className = (element.className + ` ${className}`).trim()); 35 | 36 | const removeClassName = (element: HTMLElement, className: string) => 37 | void (element.className = element.className 38 | .split(" ") 39 | .filter(cls => cls !== className) 40 | .join(" ")); 41 | 42 | const useIsomorphicLayoutEffect = 43 | typeof window === "undefined" ? React.useEffect : React.useLayoutEffect; 44 | 45 | const useDarkMode = ( 46 | options?: Options 47 | ): { 48 | isDarkMode: boolean; 49 | enable: () => void; 50 | disable: () => void; 51 | toggle: () => void; 52 | } => { 53 | const { 54 | getStorage, 55 | initialState, 56 | storageKey = "utilityjs-dark-mode", 57 | toggleClassName = "dark-mode" 58 | } = options || {}; 59 | 60 | const [prefersDark] = useMediaQuery("(prefers-color-scheme: dark)"); 61 | 62 | const [state, setState] = usePersistedState(initialState, { 63 | name: storageKey, 64 | getStorage 65 | }); 66 | 67 | const darkMode = typeof state === "undefined" ? prefersDark : state; 68 | 69 | useIsomorphicLayoutEffect(() => { 70 | if (darkMode) addClassName(document.body, toggleClassName); 71 | else removeClassName(document.body, toggleClassName); 72 | }, [darkMode]); 73 | 74 | return { 75 | isDarkMode: darkMode, 76 | /* eslint-disable react-hooks/exhaustive-deps */ 77 | enable: React.useCallback(() => setState(true), []), 78 | disable: React.useCallback(() => setState(false), []), 79 | toggle: () => setState(!darkMode) 80 | }; 81 | }; 82 | 83 | export default useDarkMode; 84 | -------------------------------------------------------------------------------- /src/hook/useDeterministicId/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useDeterministicId 4 |

5 |
6 | 7 |
8 | 9 | A React hook that generates a deterministic unique ID once per component. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-deterministic-id?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-deterministic-id) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-deterministic-id?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-deterministic-id) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-deterministic-id?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-deterministic-id) 15 | 16 | ```bash 17 | npm i @utilityjs/use-deterministic-id | yarn add @utilityjs/use-deterministic-id 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | This hook in React versions less than 18 relies on hydration to avoid server/client mismatch errors.\ 25 | If you'd like to generate ids server-side, we suggest upgrading to React 18. 26 | 27 |
28 | 29 | ## Usage 30 | 31 | ```tsx 32 | const App = (props) => { 33 | const id = useDeterministicId(); 34 | // or useDeterministicId(props.id) 35 | 36 | return ( 37 |
38 | 39 | 40 |
41 | ); 42 | }; 43 | ``` 44 | 45 | ## API 46 | 47 | ### `useDeterministicId(idOverride?, prefix?)` 48 | 49 | ```ts 50 | declare const useDeterministicId: (idOverride?: string, prefix?: string) => string; 51 | ``` 52 | 53 | #### `idOverride` 54 | 55 | Allows you to override the generated id with your own id. 56 | 57 | #### `prefix` 58 | 59 | Allows you to prefix the generated id. 60 | -------------------------------------------------------------------------------- /src/hook/useDeterministicId/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useDeterministicId"; 2 | -------------------------------------------------------------------------------- /src/hook/useDeterministicId/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-deterministic-id", 3 | "version": "1.2.0", 4 | "description": "A React hook that generates a deterministic unique ID once per component.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/useDeterministicId" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useDeterministicId", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "id", 25 | "deterministic id", 26 | "use id", 27 | "id hook", 28 | "unique id hook", 29 | "unique id" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/hook/useDeterministicId/useDeterministicId.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const DEFAULT_PREFIX = "UTILITYJS-GEN-ID"; 4 | let globalId = 0; 5 | 6 | /* eslint-disable */ 7 | const useReactId: () => string | undefined = 8 | // We use `toString()` to prevent bundlers from trying to `import { useId } from "react"` 9 | string)>(React)["useId".toString()] ?? 10 | (() => void 0); 11 | /* eslint-enable */ 12 | 13 | const prefixStr = (str: string, prefix: string) => 14 | `${prefix.length ? prefix : DEFAULT_PREFIX}-${str}`; 15 | 16 | /** 17 | * In `React < 18`: This hook relies on hydration to avoid server/client mismatch errors. 18 | */ 19 | const useDeterministicId = ( 20 | idOverride?: string, 21 | prefix = DEFAULT_PREFIX 22 | ): string => { 23 | const reactId = useReactId(); 24 | const inputId = 25 | idOverride ?? (reactId ? prefixStr(reactId, prefix) : undefined); 26 | 27 | const [defaultId, setDefaultId] = React.useState(inputId); 28 | const id = inputId ?? defaultId; 29 | 30 | React.useEffect(() => { 31 | if (id != null) return; 32 | // Fallback to this default id when possible. 33 | // Use the incrementing value for client-side rendering only. 34 | // We can't use it server-side. 35 | globalId += 1; 36 | setDefaultId(prefixStr(String(globalId), prefix)); 37 | }, [id, prefix]); 38 | 39 | return id ?? ""; 40 | }; 41 | 42 | export default useDeterministicId; 43 | -------------------------------------------------------------------------------- /src/hook/useEventListener/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useEventListener 4 |

5 |
6 | 7 |
8 | 9 | A React hook that handles binding/unbinding event listeners in a smart way. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-event-listener?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-event-listener) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-event-listener?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-event-listener) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-event-listener?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-event-listener) 15 | 16 | ```bash 17 | npm i @utilityjs/use-event-listener | yarn add @utilityjs/use-event-listener 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```ts 27 | useEventListener({ 28 | target: document, 29 | eventType: "click", 30 | handler: event => console.log(event) 31 | }); 32 | 33 | useEventListener({ 34 | target: window, 35 | eventType: "click", 36 | handler: event => console.log(event) 37 | }); 38 | 39 | useEventListener({ 40 | target: document.getElementById("target"), 41 | eventType: "click", 42 | handler: event => console.log(event) 43 | }); 44 | ``` 45 | 46 | ## API 47 | 48 | ### `useEventListener(config, shouldAttach?)` 49 | 50 | ```ts 51 | type UseEventListener = { 52 | ( 53 | config: { 54 | target: T | null; 55 | eventType: K; 56 | handler: DocumentEventListener; 57 | options?: Options; 58 | }, 59 | shouldAttach?: boolean 60 | ): void; 61 | ( 62 | config: { 63 | target: T | null; 64 | eventType: K; 65 | handler: WindowEventListener; 66 | options?: Options; 67 | }, 68 | shouldAttach?: boolean 69 | ): void; 70 | ( 71 | config: { 72 | target: React.RefObject | T | null; 73 | eventType: K; 74 | handler: ElementEventListener; 75 | options?: Options; 76 | }, 77 | shouldAttach?: boolean 78 | ): void; 79 | }; 80 | 81 | declare const useEventListener: UseEventListener; 82 | ``` 83 | 84 | #### `config` 85 | 86 | ##### `config.target` 87 | 88 | The target to which the listener will be attached. 89 | 90 | ##### `config.eventType` 91 | 92 | A case-sensitive string representing the event type to listen for. 93 | 94 | ##### `config.handler` 95 | 96 | See [The event listener callback](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#The_event_listener_callback) for details on the callback itself. 97 | 98 | ##### `config.options` 99 | 100 | See [The event listener callback](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Parameters) for details on the third parameter. 101 | 102 | #### `shouldAttach` 103 | 104 | If set to `false`, the listener won't be attached. (default = true) 105 | -------------------------------------------------------------------------------- /src/hook/useEventListener/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useEventListener"; 2 | -------------------------------------------------------------------------------- /src/hook/useEventListener/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-event-listener", 3 | "version": "1.0.6", 4 | "description": "A React hook that handles binding/unbinding event listeners in a smart way.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/useEventListener" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useEventListener", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "dependencies": { 20 | "@utilityjs/use-get-latest": "^1.0.2" 21 | }, 22 | "keywords": [ 23 | "javascript", 24 | "typescript", 25 | "react", 26 | "react hook", 27 | "event listener", 28 | "use event listener" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/hook/useEventListener/useEventListener.ts: -------------------------------------------------------------------------------- 1 | import useLatest from "@utilityjs/use-get-latest"; 2 | import * as React from "react"; 3 | 4 | type ElementEventListener = ( 5 | this: HTMLElement, 6 | ev: HTMLElementEventMap[K] 7 | ) => void; 8 | 9 | type DocumentEventListener = ( 10 | this: Document, 11 | ev: DocumentEventMap[K] 12 | ) => void; 13 | 14 | type WindowEventListener = ( 15 | this: Window, 16 | ev: WindowEventMap[K] 17 | ) => void; 18 | 19 | type Options = boolean | AddEventListenerOptions; 20 | 21 | type UseEventListener = { 22 | ( 23 | config: { 24 | target: React.RefObject | T | null; 25 | eventType: K; 26 | handler: ElementEventListener; 27 | options?: Options; 28 | }, 29 | shouldAttach?: boolean 30 | ): void; 31 | ( 32 | config: { 33 | target: T | null; 34 | eventType: K; 35 | handler: DocumentEventListener; 36 | options?: Options; 37 | }, 38 | shouldAttach?: boolean 39 | ): void; 40 | ( 41 | config: { 42 | target: T | null; 43 | eventType: K; 44 | handler: WindowEventListener; 45 | options?: Options; 46 | }, 47 | shouldAttach?: boolean 48 | ): void; 49 | }; 50 | 51 | const isOptionParamSupported = (): boolean => { 52 | let optionSupported = false; 53 | // eslint-disable-next-line @typescript-eslint/no-empty-function 54 | const fn = () => {}; 55 | 56 | try { 57 | const opt = Object.defineProperty({}, "passive", { 58 | get: () => { 59 | optionSupported = true; 60 | return null; 61 | } 62 | }); 63 | 64 | window.addEventListener("test", fn, opt); 65 | window.removeEventListener("test", fn, opt); 66 | } catch (e) { 67 | return false; 68 | } 69 | 70 | return optionSupported; 71 | }; 72 | 73 | const useEventListener: UseEventListener = ( 74 | config: { 75 | target: 76 | | React.RefObject 77 | | HTMLElement 78 | | Window 79 | | Document 80 | | null; 81 | eventType: string; 82 | handler: unknown; 83 | options?: Options; 84 | }, 85 | shouldAttach = true 86 | ): void => { 87 | const { target = null, eventType, handler, options } = config; 88 | 89 | const cachedOptions = useLatest(options); 90 | const cachedHandler = useLatest(handler); 91 | 92 | React.useEffect(() => { 93 | const element = target && "current" in target ? target.current : target; 94 | 95 | if (!element) return; 96 | 97 | let unsubscribed = false; 98 | const listener = (event: Event) => { 99 | if (unsubscribed) return; 100 | (cachedHandler.current as (ev: Event) => void)(event); 101 | }; 102 | 103 | let thirdParam = cachedOptions.current; 104 | 105 | if (typeof cachedOptions.current !== "boolean") { 106 | if (isOptionParamSupported()) thirdParam = cachedOptions.current; 107 | else thirdParam = cachedOptions.current?.capture; 108 | } 109 | 110 | shouldAttach && element.addEventListener(eventType, listener, thirdParam); 111 | 112 | return () => { 113 | unsubscribed = true; 114 | element.removeEventListener(eventType, listener, thirdParam); 115 | }; 116 | // eslint-disable-next-line react-hooks/exhaustive-deps 117 | }, [target, eventType, shouldAttach]); 118 | }; 119 | 120 | export default useEventListener; 121 | -------------------------------------------------------------------------------- /src/hook/useForceRerender/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useForceRerender 4 |

5 |
6 | 7 |
8 | 9 | A React hook that returns a function that will re-render your component when called.\ 10 | Useful when logic relies on state not represented in "React state". 11 | 12 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 13 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-force-rerender?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-force-rerender) 14 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-force-rerender?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-force-rerender) 15 | [![types](https://img.shields.io/npm/types/@utilityjs/use-force-rerender?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-force-rerender) 16 | 17 | ```bash 18 | npm i @utilityjs/use-force-rerender | yarn add @utilityjs/use-force-rerender 19 | ``` 20 | 21 |
22 | 23 |
24 | 25 | ## Usage 26 | 27 | ```ts 28 | const Component = () => { 29 | const forceRerender = useForceRerender(); 30 | 31 | React.useEffect(() => void DataStore.subscribe(() => void forceRerender()), []); 32 | 33 | return <>{DataStore.data}; 34 | } 35 | ``` 36 | 37 | ## API 38 | 39 | ### `useForceRerender()` 40 | 41 | ```ts 42 | declare const useForceRerender: () => (() => void); 43 | ``` 44 | -------------------------------------------------------------------------------- /src/hook/useForceRerender/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useForceRerender"; 2 | -------------------------------------------------------------------------------- /src/hook/useForceRerender/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-force-rerender", 3 | "version": "1.0.0", 4 | "description": "A React hook that returns a function that will re-render your component when called.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/useForceRerender" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useForceRerender", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "force update", 25 | "force update hook", 26 | "use force update", 27 | "force rerender", 28 | "force rerender hook", 29 | "use force rerender" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/hook/useForceRerender/useForceRerender.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const useForceRerender = (): (() => void) => { 4 | const [, setState] = React.useState({}); 5 | return React.useCallback(() => void setState({}), []); 6 | }; 7 | 8 | export default useForceRerender; 9 | -------------------------------------------------------------------------------- /src/hook/useForkedRefs/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useForkedRefs 4 |

5 |
6 | 7 |
8 | 9 | A React hook for forking/merging multiple refs into a single one. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-forked-refs?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-forked-refs) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-forked-refs?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-forked-refs) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-forked-refs?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-forked-refs) 15 | 16 | ```bash 17 | npm i @utilityjs/use-forked-refs | yarn add @utilityjs/use-forked-refs 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```jsx 27 | import * as React from "react"; 28 | import useForkedRefs from "@utilityjs/use-forked-refs"; 29 | 30 | const MyComponent = React.forwardRef((props, ref) => { 31 | const rootRef = React.useRef(null); 32 | const handleRef = useForkedRefs(ref, rootRef); 33 | 34 | return
; 35 | }); 36 | ``` 37 | 38 | ## API 39 | 40 | ### `useForkedRefs(...refs)` 41 | 42 | ```ts 43 | declare const useForkedRefs: (...refs: React.Ref[]) => (instance: T | null) => void; 44 | ``` 45 | 46 | #### `refs` 47 | 48 | React callback refs or refs created with `useRef()` or `createRef()`. 49 | -------------------------------------------------------------------------------- /src/hook/useForkedRefs/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useForkedRefs"; 2 | -------------------------------------------------------------------------------- /src/hook/useForkedRefs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-forked-refs", 3 | "version": "1.0.2", 4 | "description": "A React hook for forking/merging multiple refs into a single one.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/useForkedRefs" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useForkedRefs", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "utility", 23 | "fork refs", 24 | "merge ref", 25 | "merge two refs", 26 | "combine refs" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/hook/useForkedRefs/useForkedRefs.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const setRef = (ref: React.Ref, value: T) => { 4 | if (typeof ref === "function") ref(value); 5 | else if (ref && typeof ref === "object" && "current" in ref) 6 | (ref as React.MutableRefObject).current = value; 7 | }; 8 | 9 | const useForkedRefs = (...refs: React.Ref[]): React.RefCallback => 10 | React.useCallback( 11 | (instance: T) => void refs.forEach(ref => void setRef(ref, instance)), 12 | [refs] 13 | ); 14 | 15 | export default useForkedRefs; 16 | -------------------------------------------------------------------------------- /src/hook/useGetLatest/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useGetLatest 4 |

5 |
6 | 7 |
8 | 9 | A React hook that stores & updates `ref.current` with the most recent value. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-get-latest?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-get-latest) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-get-latest?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-get-latest) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-get-latest?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-get-latest) 15 | 16 | ```bash 17 | npm i @utilityjs/use-get-latest | yarn add @utilityjs/use-get-latest 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```jsx 27 | import useGetLatest from "@utilityjs/use-get-latest"; 28 | import * as React from "react"; 29 | 30 | const useAttachDomClick = (callback) => { 31 | const cachedCallback = useGetLatest(callback); 32 | 33 | React.useEffect(() => { 34 | document.addEventListener("click", cachedCallback.current); 35 | return () => { 36 | document.removeEventListener("click", cachedCallback.current); 37 | } 38 | }, []) 39 | }; 40 | ``` 41 | 42 | ## API 43 | 44 | ### `useGetLatest(value)` 45 | 46 | ```ts 47 | declare const useGetLatest: (value: T) => MutableRefObject; 48 | ``` 49 | 50 | #### `value` 51 | 52 | The value to be stored. 53 | -------------------------------------------------------------------------------- /src/hook/useGetLatest/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useGetLatest"; 2 | -------------------------------------------------------------------------------- /src/hook/useGetLatest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-get-latest", 3 | "version": "1.0.2", 4 | "description": "A React hook that stores & updates `ref.current` with the most recent value.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/useGetLatest" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useGetLatest", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "ref", 25 | "useref", 26 | "update ref", 27 | "latest ref value" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/hook/useGetLatest/useGetLatest.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const useGetLatest = (value: T): React.MutableRefObject => { 4 | const ref = React.useRef(value); 5 | 6 | React.useEffect(() => void (ref.current = value)); 7 | 8 | return ref; 9 | }; 10 | 11 | export default useGetLatest; 12 | -------------------------------------------------------------------------------- /src/hook/useGetScrollbarWidth/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useGetScrollbarWidth 4 |

5 |
6 | 7 |
8 | 9 | A React hook that calculates the width of the user agent's scrollbar. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-get-scrollbar-width?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-get-scrollbar-width) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-get-scrollbar-width?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-get-scrollbar-width) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-get-scrollbar-width?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-get-scrollbar-width) 15 | 16 | ```bash 17 | npm i @utilityjs/use-get-scrollbar-width | yarn add @utilityjs/use-get-scrollbar-width 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```tsx 27 | const App: React.FC = () => { 28 | const getScrollbarWidth = useGetScrollbarWidth(); 29 | 30 | return ( 31 |
32 | 33 |
34 | ); 35 | }; 36 | ``` 37 | 38 | ## API 39 | 40 | ### `useGetScrollbarWidth()` 41 | 42 | ```ts 43 | declare const useGetScrollbarWidth: (): (() => number); 44 | ``` 45 | -------------------------------------------------------------------------------- /src/hook/useGetScrollbarWidth/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useGetScrollbarWidth"; 2 | -------------------------------------------------------------------------------- /src/hook/useGetScrollbarWidth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-get-scrollbar-width", 3 | "version": "1.0.1", 4 | "description": "A React hook that calculates the width of the user agent's scrollbar.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/useGetScrollbarWidth" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useGetScrollbarWidth", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "scrollbar width", 25 | "scrollbar size", 26 | "use scrollbar width", 27 | "use scrollbar size" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/hook/useGetScrollbarWidth/useGetScrollbarWidth.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const useIsomorphicLayoutEffect = 4 | typeof window === "undefined" ? React.useEffect : React.useLayoutEffect; 5 | 6 | const useGetScrollbarWidth = (): (() => number) => { 7 | const scrollbarWidth = React.useRef(0); 8 | 9 | useIsomorphicLayoutEffect(() => { 10 | const dummy = document.createElement("div"); 11 | 12 | dummy.style.width = "100px"; 13 | dummy.style.height = "100px"; 14 | dummy.style.overflow = "scroll"; 15 | dummy.style.position = "absolute"; 16 | dummy.style.top = "-9999px"; 17 | dummy.setAttribute("aria-hidden", "true"); 18 | dummy.setAttribute("role", "presentation"); 19 | 20 | document.body.appendChild(dummy); 21 | scrollbarWidth.current = dummy.offsetWidth - dummy.clientWidth; 22 | document.body.removeChild(dummy); 23 | }, []); 24 | 25 | const getScrollBarWidth = React.useCallback(() => scrollbarWidth.current, []); 26 | 27 | return getScrollBarWidth; 28 | }; 29 | 30 | export default useGetScrollbarWidth; 31 | -------------------------------------------------------------------------------- /src/hook/useHash/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useHash 4 |

5 |
6 | 7 |
8 | 9 | A React hook that helps to sync and modify browser's location hash. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-hash?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-hash) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-hash?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-hash) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-hash?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-hash) 15 | 16 | ```bash 17 | npm i @utilityjs/use-hash | yarn add @utilityjs/use-hash 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```tsx 27 | import * as React from "react"; 28 | import useHash from "@utilityjs/use-hash"; 29 | 30 | const App: React.FC = () => { 31 | const hashConsumer = useHash(); 32 | 33 | const [key, setKey] = React.useState(""); 34 | const [value, setValue] = React.useState(""); 35 | 36 | const addParam = () => { 37 | hashConsumer.addParam(key, value); 38 | setKey(""); 39 | setValue(""); 40 | }; 41 | 42 | const deleteParam = () => { 43 | hashConsumer.deleteParam(key); 44 | setKey(""); 45 | setValue(""); 46 | }; 47 | 48 | const deleteValue = () => { 49 | hashConsumer.deleteParamValue(key, value); 50 | setKey(""); 51 | setValue(""); 52 | }; 53 | 54 | return
55 |
Hash: {hashConsumer.hash}
56 |
Params: {JSON.stringify(hashConsumer.getParams(), null, 2)}
57 |
58 |
59 | {" "} 60 | setKey(e.target.value)} 66 | /> 67 |
68 | {" "} 69 | setValue(e.target.value)} 75 | /> 76 |
77 |
78 | 79 | 80 | 81 | 84 |
85 |
; 86 | }; 87 | 88 | export default App; 89 | ``` 90 | 91 | ## API 92 | 93 | ### `useHash()` 94 | 95 | ```ts 96 | interface HashConsumer { 97 | /** The hash state. */ 98 | hash: string; 99 | 100 | /** The hash state updater function. */ 101 | setHash: React.Dispatch>; 102 | 103 | /** Returns a boolean value indicating if such a given parameter exists. */ 104 | hasParam: (key: string) => boolean; 105 | 106 | /** Returns all parameters as key/value pairs. */ 107 | getParams: () => Record; 108 | 109 | /** Adds a specified key/value pair as a new parameter. */ 110 | addParam: (key: string, value: string) => void; 111 | 112 | /** 113 | * Sets the value associated with a given parameter to the given value. 114 | * If there are several values, the others are deleted. 115 | */ 116 | setParam: (key: string, value: string) => void; 117 | 118 | /** Deletes the given parameter, and its associated value. */ 119 | deleteParam: (key: string) => void; 120 | 121 | /** Returns the values associated with a given parameter. */ 122 | getParamValue: (key: string) => string | string[] | null; 123 | 124 | /** 125 | * Deletes the value associated with a given parameter. 126 | * If there aren't several values, the parameter is deleted. 127 | */ 128 | deleteParamValue: (key: string, value: string) => void; 129 | } 130 | 131 | declare const useHash: () => HashConsumer; 132 | ``` 133 | -------------------------------------------------------------------------------- /src/hook/useHash/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useHash"; 2 | -------------------------------------------------------------------------------- /src/hook/useHash/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-hash", 3 | "version": "1.0.0", 4 | "description": "A React hook that helps to sync and modify browser's location hash.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/useHash" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useHash", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "hash", 25 | "use hash", 26 | "hash state", 27 | "use hash state" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/hook/useHover/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useHover 4 |

5 |
6 | 7 |
8 | 9 | A React hook that determines if the mouse is hovering an element. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-hover?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-hover) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-hover?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-hover) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-hover?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-hover) 15 | 16 | ```bash 17 | npm i @utilityjs/use-hover | yarn add @utilityjs/use-hover 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```tsx 27 | const App: React.FC = () => { 28 | const { isHovered, registerRef } = useHover(); 29 | 30 | return ( 31 |
32 |
33 | {`The current div is ${isHovered ? `hovered` : `unhovered`}`} 34 |
35 |
36 | ); 37 | }; 38 | ``` 39 | 40 | ## API 41 | 42 | ### `useHover()` 43 | 44 | ```ts 45 | declare const useHover: () => { 46 | isHovered: boolean; 47 | setIsHovered: React.Dispatch>; 48 | registerRef: (node: T | null) => void; 49 | }; 50 | ``` -------------------------------------------------------------------------------- /src/hook/useHover/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useHover"; 2 | -------------------------------------------------------------------------------- /src/hook/useHover/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-hover", 3 | "version": "1.0.2", 4 | "description": "A React hook that determines if the mouse is hovering an element.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/useHover" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useHover", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "dependencies": { 20 | "@utilityjs/use-register-node-ref": "latest" 21 | }, 22 | "keywords": [ 23 | "javascript", 24 | "typescript", 25 | "react", 26 | "react hook", 27 | "ref callback", 28 | "use hover hook", 29 | "use mouse enter", 30 | "use mouse leave" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /src/hook/useHover/useHover.ts: -------------------------------------------------------------------------------- 1 | import useRegisterNodeRef from "@utilityjs/use-register-node-ref"; 2 | import * as React from "react"; 3 | 4 | const useHover = (): { 5 | isHovered: boolean; 6 | setIsHovered: React.Dispatch>; 7 | registerRef: (node: T | null) => void; 8 | } => { 9 | const [isHovered, setIsHovered] = React.useState(false); 10 | 11 | const handleMouseEnter = () => setIsHovered(true); 12 | const handleMouseLeave = () => setIsHovered(false); 13 | 14 | const registerRef = useRegisterNodeRef(node => { 15 | node.addEventListener("mouseenter", handleMouseEnter); 16 | node.addEventListener("mouseleave", handleMouseLeave); 17 | 18 | return () => { 19 | node.removeEventListener("mouseenter", handleMouseEnter); 20 | node.removeEventListener("mouseleave", handleMouseLeave); 21 | }; 22 | }); 23 | 24 | return { isHovered, setIsHovered, registerRef }; 25 | }; 26 | 27 | export default useHover; 28 | -------------------------------------------------------------------------------- /src/hook/useImmutableArray/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useImmutableArray 4 |

5 |
6 | 7 |
8 | 9 | A React hook that creates an array with immutable operations. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-immutable-array?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-immutable-array) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-immutable-array?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-immutable-array) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-immutable-array?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-immutable-array) 15 | 16 | ```bash 17 | npm i @utilityjs/use-immutable-array | yarn add @utilityjs/use-immutable-array 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```ts 27 | const array = useImmutableArray([1, 2, 3, 4, 5]); 28 | 29 | array.push(6) // [1, 2, 3, 4, 5, 6] 30 | array.pop() // [1, 2, 3, 4, 5] 31 | array.shift() // [2, 3, 4, 5] 32 | array.shift(10) // [10, 2, 3, 4, 5] 33 | array.reverse() // [5, 4, 3, 2, 10] 34 | array.removeByIndex(1) // [5, 3, 2, 10] 35 | array.removeByValue(10) // [5, 3, 2] 36 | array.filter(item => item % 2 !== 0) // [5, 3] 37 | array.insertItem(1, 7) // [5, 7, 3] 38 | array.moveItem(0, 1) // [7, 5, 3] 39 | array.values // [7, 5, 3] 40 | array.setValues([1, 2, 3, 4]) // [1, 2, 3, 4] 41 | ``` 42 | 43 | ## API 44 | 45 | ### `useImmutableArray(array)` 46 | 47 | ```ts 48 | interface Return { 49 | pop: () => void; 50 | push: (value: T) => void; 51 | shift: () => void; 52 | unshift: (value: T) => void; 53 | reverse: () => void; 54 | removeByIndex: (index: number) => void; 55 | removeByValue: (value: T) => void; 56 | filter: ( 57 | predicate: (value: T, index: number, array: T[]) => value is T, 58 | thisArg?: any 59 | ) => void; 60 | insertItem: (index: number, value: T) => void; 61 | moveItem: (fromIndex: number, toIndex: number) => void; 62 | values: T[]; 63 | setValues: SetArrayValues; 64 | } 65 | 66 | declare const useImmutableArray: (array: T[]) => Return; 67 | ``` 68 | 69 | #### `array` 70 | 71 | The initial value of the immutable-array. 72 | -------------------------------------------------------------------------------- /src/hook/useImmutableArray/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useImmutableArray"; 2 | -------------------------------------------------------------------------------- /src/hook/useImmutableArray/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-immutable-array", 3 | "version": "1.0.1", 4 | "description": "A React hook that creates an array with immutable operations.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/useImmutableArray" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useImmutableArray", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "immutable", 25 | "array", 26 | "immutable array" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/hook/useImmutableArray/useImmutableArray.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type SetArrayValues = React.Dispatch>; 4 | 5 | type Pop = () => void; 6 | type Push = (value: T) => void; 7 | type Shift = () => void; 8 | type Unshift = (value: T) => void; 9 | type Reverse = () => void; 10 | type RemoveByIndex = (index: number) => void; 11 | type RemoveByValue = (value: T) => void; 12 | type Filter = ( 13 | predicate: (value: T, index: number, array: T[]) => value is T, 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | thisArg?: any 16 | ) => void; 17 | type InsertItem = (index: number, value: T) => void; 18 | type MoveItem = (fromIndex: number, toIndex: number) => void; 19 | 20 | const makePop = 21 | (array: T[], setValues: SetArrayValues): Pop => 22 | () => 23 | setValues(array.slice(0, -1)); 24 | 25 | const makePush = 26 | (array: T[], setValues: SetArrayValues): Push => 27 | (value: T) => 28 | setValues([...array, value]); 29 | 30 | const makeShift = 31 | (array: T[], setValues: SetArrayValues): Shift => 32 | () => 33 | setValues(array.slice(1)); 34 | 35 | const makeUnshift = 36 | (array: T[], setValues: SetArrayValues): Unshift => 37 | (value: T) => 38 | setValues([value, ...array]); 39 | 40 | const makeReverse = 41 | (array: T[], setValues: SetArrayValues): Reverse => 42 | () => 43 | setValues([...array].reverse()); 44 | 45 | const makeRemoveByIndex = 46 | (array: T[], setValues: SetArrayValues): RemoveByIndex => 47 | (index: number) => 48 | setValues([...array.slice(0, index), ...array.slice(index + 1)]); 49 | 50 | const makeRemoveByValue = 51 | (array: T[], setValues: SetArrayValues): RemoveByValue => 52 | (value: T) => 53 | setValues(array.filter(item => item !== value)); 54 | 55 | const makeFilter = 56 | (array: T[], setValues: SetArrayValues): Filter => 57 | (predicate, thisArg) => 58 | setValues(array.filter(predicate, thisArg)); 59 | 60 | const makeInsertItem = 61 | (array: T[], setValues: SetArrayValues): InsertItem => 62 | (index: number, value: T) => 63 | setValues([...array.slice(0, index), value, ...array.slice(index)]); 64 | 65 | const makeMoveItem = 66 | (array: T[], setValues: SetArrayValues): MoveItem => 67 | (fromIndex: number, toIndex: number) => { 68 | if (fromIndex > toIndex) { 69 | setValues([ 70 | ...array.slice(0, toIndex), 71 | array[fromIndex], 72 | ...array.slice(toIndex + 1, fromIndex), 73 | array[toIndex], 74 | ...array.slice(fromIndex + 1) 75 | ]); 76 | } else makeMoveItem(array, setValues)(toIndex, fromIndex); 77 | }; 78 | 79 | interface Return { 80 | pop: Pop; 81 | push: Push; 82 | shift: Shift; 83 | unshift: Unshift; 84 | reverse: Reverse; 85 | removeByIndex: RemoveByIndex; 86 | removeByValue: RemoveByValue; 87 | filter: Filter; 88 | insertItem: InsertItem; 89 | moveItem: MoveItem; 90 | values: T[]; 91 | setValues: SetArrayValues; 92 | } 93 | 94 | const useImmutableArray = (array: T[]): Return => { 95 | const [values, setValues] = React.useState(array); 96 | 97 | return React.useMemo( 98 | () => ({ 99 | pop: makePop(values, setValues), 100 | push: makePush(values, setValues), 101 | shift: makeShift(values, setValues), 102 | unshift: makeUnshift(values, setValues), 103 | reverse: makeReverse(values, setValues), 104 | removeByIndex: makeRemoveByIndex(values, setValues), 105 | removeByValue: makeRemoveByValue(values, setValues), 106 | filter: makeFilter(values, setValues), 107 | insertItem: makeInsertItem(values, setValues), 108 | moveItem: makeMoveItem(values, setValues), 109 | values, 110 | setValues 111 | }), 112 | [values] 113 | ); 114 | }; 115 | 116 | export default useImmutableArray; 117 | -------------------------------------------------------------------------------- /src/hook/useIsInViewport/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useIsInViewport 4 |

5 |
6 | 7 |
8 | 9 | A React hook that tells you when an element enters or leaves the viewport.\ 10 | (Reuses observer instances where possible) 11 | 12 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 13 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-is-in-viewport?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-is-in-viewport) 14 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-is-in-viewport?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-is-in-viewport) 15 | [![types](https://img.shields.io/npm/types/@utilityjs/use-is-in-viewport?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-is-in-viewport) 16 | 17 | ```bash 18 | npm i @utilityjs/use-is-in-viewport | yarn add @utilityjs/use-is-in-viewport 19 | ``` 20 | 21 |
22 | 23 |
24 | 25 | ## Usage 26 | 27 | ```tsx 28 | const App: React.FC = () => { 29 | const { registerNode, isInViewport } = useIsInViewport(); 30 | 31 | console.log(`is red box in the viewport? ${isInViewport}`); 32 | 33 | return ( 34 |
35 |
36 |
37 |
38 |
45 |
46 |
47 |
48 | ); 49 | }; 50 | ``` 51 | 52 | ## API 53 | 54 | ### `useIsInViewport( options?)` 55 | 56 | ```ts 57 | interface HookOptions { 58 | once?: boolean; 59 | disabled?: boolean; 60 | } 61 | 62 | interface HookConsumer { 63 | registerNode: (node: T | null) => void; 64 | isInViewport: boolean; 65 | } 66 | 67 | declare const useIsInViewport: ( 68 | options?: (IntersectionObserverInit & HookOptions) | undefined 69 | ) => HookConsumer; 70 | ``` 71 | 72 | #### `options.once` - (default: `false`) 73 | 74 | Only trigger the callback once. (unless you have toggled `disabled` option.) 75 | 76 | #### `options.disabled` - (default: `false`) 77 | 78 | Skip creating the observer.\ 79 | You can use this to enable and disable the observer as needed. 80 | 81 | #### `options.threshold` - (default: `[0, 1]`) 82 | 83 | Read the [MDN Web Doc](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#intersection_observer_options) 84 | 85 | #### `options.root` - (default: `null`) 86 | 87 | Read the [MDN Web Doc](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#intersection_observer_options) 88 | 89 | #### `options.rootMargin` - (default: `0px`) 90 | 91 | Read the [MDN Web Doc](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#intersection_observer_options) -------------------------------------------------------------------------------- /src/hook/useIsInViewport/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useIsInViewport"; 2 | -------------------------------------------------------------------------------- /src/hook/useIsInViewport/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-is-in-viewport", 3 | "version": "1.0.0", 4 | "description": "A React hook that tells you when an element enters or leaves the viewport.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/useIsInViewport" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useIsInViewport", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "dependencies": { 20 | "@utilityjs/use-register-node-ref": "^1.1.0" 21 | }, 22 | "keywords": [ 23 | "javascript", 24 | "typescript", 25 | "react", 26 | "react hook", 27 | "viewport", 28 | "lazy-loading", 29 | "intersection", 30 | "observer", 31 | "intersection observer", 32 | "isInViewport" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /src/hook/useIsMounted/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useIsMounted 4 |

5 |
6 | 7 |
8 | 9 | A React hook that returns `true` if the component is mounted. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-is-mounted?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-is-mounted) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-is-mounted?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-is-mounted) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-is-mounted?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-is-mounted) 15 | 16 | ```bash 17 | npm i @utilityjs/use-is-mounted | yarn add @utilityjs/use-is-mounted 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```ts 27 | import useGetLatest from "@utilityjs/use-get-latest"; 28 | import useIsMounted from "@utilityjs/use-is-mounted"; 29 | import * as React from "react"; 30 | 31 | const useHook = (callback: () => void) => { 32 | const isMounted = useIsMounted(); 33 | 34 | const cachedCallback = useGetLatest(callback); 35 | 36 | React.useEffect(() => { 37 | const cb = cachedCallback.current(); 38 | if (isMounted()) cb(); 39 | }, []); 40 | }; 41 | ``` 42 | 43 | ## API 44 | 45 | ### `useIsMounted()` 46 | 47 | ```ts 48 | declare const useIsMounted: () => (() => boolean); 49 | ``` 50 | -------------------------------------------------------------------------------- /src/hook/useIsMounted/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useIsMounted"; 2 | -------------------------------------------------------------------------------- /src/hook/useIsMounted/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-is-mounted", 3 | "version": "1.0.2", 4 | "description": "A React hook that returns `true` if the component is mounted.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/useIsMounted" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useIsMounted", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "mounted", 25 | "is mounted", 26 | "use is mounted" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/hook/useIsMounted/useIsMounted.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const useIsMounted = (): (() => boolean) => { 4 | const isMountedRef = React.useRef(false); 5 | 6 | React.useEffect(() => { 7 | isMountedRef.current = true; 8 | return () => void (isMountedRef.current = false); 9 | }, []); 10 | 11 | return React.useCallback(() => isMountedRef.current, []); 12 | }; 13 | 14 | export default useIsMounted; 15 | -------------------------------------------------------------------------------- /src/hook/useIsServerHandoffComplete/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useIsServerHandoffComplete 4 |

5 |
6 | 7 |
8 | 9 | A React hook that returns `true` if the SSR handoff completes. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-is-server-handoff-complete?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-is-server-handoff-complete) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-is-server-handoff-complete?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-is-server-handoff-complete) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-is-server-handoff-complete?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-is-server-handoff-complete) 15 | 16 | ```bash 17 | npm i @utilityjs/use-is-server-handoff-complete | yarn add @utilityjs/use-is-server-handoff-complete 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```ts 27 | import useIsServerHandoffComplete from "@utilityjs/use-is-server-handoff-complete"; 28 | import * as React from "react"; 29 | 30 | let __ID__ = 0; 31 | 32 | const useDeterministicId = (inputId?: string) => { 33 | const handoffCompletes = useIsServerHandoffComplete(); 34 | 35 | const [id, setId] = React.useState( 36 | inputId ?? handoffCompletes ? String(__ID__++) : null 37 | ); 38 | 39 | React.useEffect(() => { 40 | if (id != null) return; 41 | setId(__ID__++); 42 | }, [id]); 43 | 44 | return id; 45 | }; 46 | ``` 47 | 48 | ## API 49 | 50 | ### `useIsServerHandoffComplete()` 51 | 52 | ```ts 53 | declare const useIsServerHandoffComplete: () => boolean; 54 | ``` 55 | -------------------------------------------------------------------------------- /src/hook/useIsServerHandoffComplete/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useIsServerHandoffComplete"; 2 | -------------------------------------------------------------------------------- /src/hook/useIsServerHandoffComplete/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-is-server-handoff-complete", 3 | "version": "1.0.0", 4 | "description": "A React hook that returns `true` if the SSR handoff completes.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/useIsServerHandoffComplete" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useIsServerHandoffComplete", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "ssr", 25 | "server handoff", 26 | "ssr handoff" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/hook/useIsServerHandoffComplete/useIsServerHandoffComplete.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const handoff = { isComplete: false }; 4 | 5 | const useIsServerHandoffComplete = (): boolean => { 6 | const [isComplete, setIsComplete] = React.useState(handoff.isComplete); 7 | 8 | React.useEffect( 9 | () => void (!isComplete && setIsComplete(true)), 10 | [isComplete] 11 | ); 12 | 13 | React.useEffect(() => { 14 | if (!handoff.isComplete) handoff.isComplete = true; 15 | }, []); 16 | 17 | return isComplete; 18 | }; 19 | 20 | export default useIsServerHandoffComplete; 21 | -------------------------------------------------------------------------------- /src/hook/useIsomorphicLayoutEffect/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useIsomorphicLayoutEffect 4 |

5 |
6 | 7 |
8 | 9 | A React hook that schedules a `React.useLayoutEffect` with a fallback to a `React.useEffect` for environments where layout effects should not be used (such as server-side rendering). 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-isomorphic-layout-effect?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-isomorphic-layout-effect) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-isomorphic-layout-effect?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-isomorphic-layout-effect) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-isomorphic-layout-effect?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-isomorphic-layout-effect) 15 | 16 | ```bash 17 | npm i @utilityjs/use-isomorphic-layout-effect | yarn add @utilityjs/use-isomorphic-layout-effect 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## API 25 | 26 | ### `useIsomorphicLayoutEffect(effect, deps?)` 27 | 28 | ```ts 29 | declare const useIsomorphicLayoutEffect: typeof React.useLayoutEffect; 30 | ``` 31 | -------------------------------------------------------------------------------- /src/hook/useIsomorphicLayoutEffect/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useIsomorphicLayoutEffect"; 2 | -------------------------------------------------------------------------------- /src/hook/useIsomorphicLayoutEffect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-isomorphic-layout-effect", 3 | "version": "1.0.2", 4 | "description": "A React hook that schedules a `React.useLayoutEffect` with a fallback to a `React.useEffect` for environments where layout effects should not be used (such as server-side rendering).", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/useIsomorphicLayoutEffect" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useIsomorphicLayoutEffect", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "layout effect", 25 | "isomorphic", 26 | "isomorphic layout effect" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/hook/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const useIsomorphicLayoutEffect = 4 | typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect; 5 | 6 | export default useIsomorphicLayoutEffect; 7 | -------------------------------------------------------------------------------- /src/hook/useKeybind/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useKeybind"; 2 | export { default } from "./useKeybind"; 3 | -------------------------------------------------------------------------------- /src/hook/useKeybind/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-keybind", 3 | "version": "1.0.4", 4 | "description": "A React hook for invoking a callback when hotkeys are pressed.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/useKeybind" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useKeybind", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "dependencies": { 20 | "@utilityjs/use-event-listener": "^1.0.6" 21 | }, 22 | "keywords": [ 23 | "javascript", 24 | "typescript", 25 | "react", 26 | "react hook", 27 | "keybind", 28 | "hotkey", 29 | "use hotkey", 30 | "use keybind", 31 | "shortcut", 32 | "use shortcut", 33 | "shortkey", 34 | "use shortkey" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/hook/useLazyInitializedValue/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useLazyInitializedValue 4 |

5 |
6 | 7 |
8 | 9 | A React hook that holds a lazy-initialized value for a component's lifecycle. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-lazy-initialized-value?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-lazy-initialized-value) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-lazy-initialized-value?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-lazy-initialized-value) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-lazy-initialized-value?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-lazy-initialized-value) 15 | 16 | ```bash 17 | npm i @utilityjs/use-lazy-initialized-value | yarn add @utilityjs/use-lazy-initialized-value 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | This hook calls the provided `initFactory` on mount and returns the factory value for the duration of the component's lifecycle. See React docs on [creating expensive objects lazily](https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily). 25 | 26 | #### **Comparison to `useMemo`** 27 | You may rely on `useMemo` only as a performance optimization, not as a [semantic guarantee](https://reactjs.org/docs/hooks-reference.html#usememo). 28 | React may throw away the cached value and recall your factory even if deps did not change. 29 | 30 | #### **Comparison to `useState`** 31 | You can get the same result using `useState(factory)[0]`, but it's a little more expensive supporting unused update functionality. 32 | 33 | The right way is to implement it using `useRef` as described in React doc's [how to create expensive objects lazily](https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily). However, `useLazyInitializedValue` is likely more convenient and hides the `ref.current` implementation detail. 34 | 35 |
36 | 37 | ## Usage 38 | 39 | ```tsx 40 | const Component = () => { 41 | // Creating expensive object lazily 42 | // Can guarantee that `obj` is always same instance 43 | const obj = useLazyInitializedValue(() => { 44 | return createExpensiveObject(); 45 | }); 46 | 47 | // ... 48 | } 49 | ``` 50 | 51 | ## API 52 | 53 | ### `useLazyInitializedValue(initFactory)` 54 | 55 | ```ts 56 | declare const useLazyInitializedValue: (initFactory: () => T) => T; 57 | ``` -------------------------------------------------------------------------------- /src/hook/useLazyInitializedValue/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useLazyInitializedValue"; 2 | -------------------------------------------------------------------------------- /src/hook/useLazyInitializedValue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-lazy-initialized-value", 3 | "version": "1.0.0", 4 | "description": "A React hook that holds a lazy-initialized value for a component's lifecycle.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/useLazyInitializedValue" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useLazyInitializedValue", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "lazy initialization", 25 | "lazy initialization hook", 26 | "lazy initialized value", 27 | "lazy initialized value hook", 28 | "use lazy initialized value", 29 | "lazy value", 30 | "lazy value hook", 31 | "use lazy value" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/hook/useLazyInitializedValue/useLazyInitializedValue.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const __SENTINEL__ = {}; 4 | 5 | const useLazyInitializedValue = (initFactory: () => T): T => { 6 | const lazyValue = React.useRef(__SENTINEL__); 7 | 8 | if (lazyValue.current === __SENTINEL__) lazyValue.current = initFactory(); 9 | return lazyValue.current; 10 | }; 11 | 12 | export default useLazyInitializedValue; 13 | -------------------------------------------------------------------------------- /src/hook/useLongPress/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useLongPress 4 |

5 |
6 | 7 |
8 | 9 | A React hook that detects long clicks/taps. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-long-press?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-long-press) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-long-press?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-long-press) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-long-press?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-long-press) 15 | 16 | ```bash 17 | npm i @utilityjs/use-long-press | yarn add @utilityjs/use-long-press 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```tsx 27 | const App: React.FC = () => { 28 | const { registerNode } = useLongPress( 29 | () => { 30 | console.log("long pressed"); 31 | }, 32 | { preventLongPressOnMove: true, preventContextMenuOnLongPress: true } 33 | ); 34 | 35 | return ( 36 |
37 |
41 |
42 | ); 43 | }; 44 | ``` 45 | 46 | ## API 47 | 48 | ### `useLongPress(callback, options?)` 49 | 50 | ```ts 51 | interface Options { 52 | pressDelay?: number; 53 | moveThreshold?: number; 54 | preventContextMenuOnLongPress?: boolean; 55 | preventLongPressOnMove?: boolean; 56 | } 57 | 58 | interface HookReturn { 59 | registerNode: (node: T | null) => void; 60 | } 61 | 62 | declare const useLongPress: (callback: () => void, options?: Options) => HookReturn; 63 | 64 | ``` 65 | 66 | #### `callback` 67 | 68 | The callback is called when the long press happened. 69 | 70 | #### `options.pressDelay` - default: 500 71 | 72 | The time (in miliseconds) user need to hold click or tap before long press callback is triggered. 73 | 74 | #### `options.moveThreshold` - default: 25 75 | 76 | The move tolerance in pixels. 77 | 78 | #### `options.preventContextMenuOnLongPress` - default: false 79 | 80 | Determines whether default context menu should be cancelled when long press happened. 81 | 82 | #### `options.preventLongPressOnMove` - default: false 83 | 84 | Determines whether long press should be cancelled when detected movement while pressing. -------------------------------------------------------------------------------- /src/hook/useLongPress/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useLongPress"; 2 | -------------------------------------------------------------------------------- /src/hook/useLongPress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-long-press", 3 | "version": "1.0.2", 4 | "description": "A React hook that detects long clicks/taps.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/useLongPress" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useLongPress", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "dependencies": { 20 | "@utilityjs/use-get-latest": "^1.0.2", 21 | "@utilityjs/use-register-node-ref": "^1.1.0" 22 | }, 23 | "keywords": [ 24 | "javascript", 25 | "typescript", 26 | "react", 27 | "react hook", 28 | "long press", 29 | "long touch", 30 | "long tap", 31 | "use long press", 32 | "use long touch", 33 | "use long tap" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/hook/useLongPress/useLongPress.ts: -------------------------------------------------------------------------------- 1 | import useGetLatest from "@utilityjs/use-get-latest"; 2 | import useRegisterNodeRef from "@utilityjs/use-register-node-ref"; 3 | import * as React from "react"; 4 | 5 | type TargetEvent = MouseEvent | TouchEvent; 6 | 7 | type Coordinates = { x: number; y: number }; 8 | 9 | interface Options { 10 | pressDelay?: number; 11 | moveThreshold?: number; 12 | preventContextMenuOnLongPress?: boolean; 13 | preventLongPressOnMove?: boolean; 14 | } 15 | 16 | interface HookReturn { 17 | registerNode: (node: T | null) => void; 18 | } 19 | 20 | const isTouch = (event: Event): event is TouchEvent => 21 | TouchEvent ? event instanceof TouchEvent : "touches" in event; 22 | 23 | const isMouse = (event: Event): event is MouseEvent => 24 | event instanceof MouseEvent; 25 | 26 | const calcPosition = (event: TargetEvent): Coordinates | null => { 27 | return isTouch(event) 28 | ? { x: event.touches[0].pageX, y: event.touches[0].pageY } 29 | : isMouse(event) 30 | ? { x: event.pageX, y: event.pageY } 31 | : null; 32 | }; 33 | 34 | const useLongPress = ( 35 | callback: () => void, 36 | options: Options = {} 37 | ): HookReturn => { 38 | const { 39 | pressDelay = 500, 40 | moveThreshold = 25, 41 | preventContextMenuOnLongPress = false, 42 | preventLongPressOnMove = false 43 | } = options; 44 | 45 | const callbackRef = useGetLatest(callback); 46 | 47 | const timeoutRef = React.useRef>(); 48 | const startPositionsRef = React.useRef(null); 49 | 50 | const isPressedRef = React.useRef(false); 51 | 52 | const startLongPress = (event: TargetEvent) => { 53 | // Ignore events other than mouse and touch 54 | if (!isMouse(event) && !isTouch(event)) return; 55 | 56 | if (isPressedRef.current) return; 57 | 58 | // Main button pressed 59 | isPressedRef.current = isMouse(event) ? event.button === 0 : true; 60 | startPositionsRef.current = calcPosition(event); 61 | 62 | timeoutRef.current = setTimeout( 63 | () => void callbackRef.current?.(), 64 | pressDelay 65 | ); 66 | }; 67 | 68 | const stopLongPress = (event: TargetEvent) => { 69 | // Ignore events other than mouse and touch 70 | if (!isMouse(event) && !isTouch(event)) return; 71 | 72 | isPressedRef.current = false; 73 | startPositionsRef.current = null; 74 | 75 | if (timeoutRef.current) clearTimeout(timeoutRef.current); 76 | }; 77 | 78 | const preventLongPress = (event: TargetEvent) => { 79 | if (!preventLongPressOnMove) return; 80 | 81 | // Ignore events other than mouse and touch 82 | if (!isMouse(event) && !isTouch(event)) return; 83 | 84 | if (!startPositionsRef.current) return; 85 | 86 | const position = calcPosition(event); 87 | const initialPosition = startPositionsRef.current; 88 | 89 | if (!position) return; 90 | 91 | const dx = Math.abs(position.x - initialPosition.x); 92 | const dy = Math.abs(position.y - initialPosition.y); 93 | 94 | if (dx > moveThreshold || dy > moveThreshold) stopLongPress(event); 95 | }; 96 | 97 | const preventContextMenu = (event: TargetEvent) => { 98 | if (!isPressedRef.current) return; 99 | if (preventContextMenuOnLongPress) event.preventDefault?.(); 100 | }; 101 | 102 | const handleEvents = (node: HTMLElement, unsubscribe = false) => { 103 | node.oncontextmenu = unsubscribe ? null : preventContextMenu; 104 | 105 | const fn = unsubscribe 106 | ? node.removeEventListener.bind(node) 107 | : node.addEventListener.bind(node); 108 | 109 | fn("mousedown", startLongPress); 110 | fn("touchstart", startLongPress); 111 | 112 | fn("mousemove", preventLongPress); 113 | fn("touchmove", preventLongPress); 114 | 115 | fn("mouseup", stopLongPress); 116 | fn("mouseleave", stopLongPress); 117 | fn("touchend", stopLongPress); 118 | }; 119 | 120 | const subscriber = (node: T) => { 121 | handleEvents(node); 122 | return () => void handleEvents(node, true); 123 | }; 124 | 125 | const registerNode = useRegisterNodeRef(subscriber); 126 | 127 | React.useEffect(() => { 128 | return () => { 129 | if (timeoutRef.current) clearTimeout(timeoutRef.current); 130 | }; 131 | }, []); 132 | 133 | return { registerNode }; 134 | }; 135 | 136 | export default useLongPress; 137 | -------------------------------------------------------------------------------- /src/hook/useMediaQuery/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useMediaQuery 4 |

5 |
6 | 7 |
8 | 9 | A React hook that helps detect whether media queries individually match. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-media-query?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-media-query) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-media-query?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-media-query) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-media-query?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-media-query) 15 | 16 | ```bash 17 | npm i @utilityjs/use-media-query | yarn add @utilityjs/use-media-query 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```tsx 27 | import * as React from "react"; 28 | import useMediaQuery from "@utilityjs/use-media-query"; 29 | 30 | const App: React.FC = () => { 31 | const [min, setMin] = React.useState(100); 32 | const [max, setMax] = React.useState(100); 33 | 34 | const matches = useMediaQuery([ 35 | `(min-width: ${min}px)`, 36 | `(max-width: ${max}px)` 37 | ]); 38 | 39 | return ( 40 |
41 | {matches.join(", ")} 42 |
43 | min:{" "} 44 | setMin(parseInt(e.target.value))} 49 | /> 50 |
51 | max:{" "} 52 | setMax(parseInt(e.target.value))} 57 | /> 58 |
59 |
60 | ); 61 | }; 62 | 63 | export default App; 64 | ``` 65 | 66 | ## API 67 | 68 | ### `useMediaQuery(query)` 69 | 70 | ```ts 71 | declare const useMediaQuery: (query: string | string[]) => boolean[]; 72 | ``` 73 | 74 | #### `query` 75 | 76 | A string or array of strings specifying the media query/queries to parse into [MediaQueryList](https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList). 77 | 78 | -------------------------------------------------------------------------------- /src/hook/useMediaQuery/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useMediaQuery"; 2 | -------------------------------------------------------------------------------- /src/hook/useMediaQuery/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-media-query", 3 | "version": "1.0.1", 4 | "description": "A React hook that helps detect whether media queries individually match.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/useMediaQuery" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useMediaQuery", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "media", 25 | "media query", 26 | "match media", 27 | "use media", 28 | "use match media", 29 | "use media query" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/hook/useMediaQuery/useMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const useIsomorphicLayoutEffect = 4 | typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect; 5 | 6 | const isSupported = typeof window !== "undefined" && "matchMedia" in window; 7 | 8 | const useMediaQuery = (query: string | string[]): boolean[] => { 9 | const queries = Array.isArray(query) ? query : [query]; 10 | const queriesKey = queries.join(); 11 | 12 | const [matches, setMatches] = React.useState( 13 | Array(queries.length).fill(false) as boolean[] 14 | ); 15 | 16 | useIsomorphicLayoutEffect(() => { 17 | if (!isSupported) { 18 | // eslint-disable-next-line no-console 19 | console.error( 20 | [ 21 | "[@utilityjs/use-media-query]: useMediaQuery relies on the `window.matchMedia` API.", 22 | "Since it is not supported in your browser, we are returning `false`." 23 | ].join("\n") 24 | ); 25 | return; 26 | } 27 | 28 | const mediaQueries = queries.map(query => matchMedia(query)); 29 | 30 | // Initial matching 31 | setMatches(mediaQueries.map(mediaQuery => mediaQuery.matches)); 32 | 33 | let unsubscribed = false; 34 | const changeHandlers = mediaQueries.map(mediaQuery => { 35 | const changeHandler = (event: MediaQueryListEvent) => { 36 | if (unsubscribed) return; 37 | 38 | const isMatched = event.matches; 39 | const queryIndex = mediaQueries.findIndex( 40 | mediaQuery => mediaQuery.media === event.media 41 | ); 42 | 43 | setMatches(prevMatches => { 44 | const clone = [...prevMatches]; 45 | clone[queryIndex] = isMatched; 46 | return clone; 47 | }); 48 | }; 49 | 50 | mediaQuery.addEventListener("change", changeHandler); 51 | 52 | return changeHandler; 53 | }); 54 | 55 | return () => { 56 | unsubscribed = true; 57 | mediaQueries.forEach((mediaQuery, index) => { 58 | mediaQuery.removeEventListener("change", changeHandlers[index]); 59 | }); 60 | }; 61 | }, [queriesKey]); 62 | 63 | return matches; 64 | }; 65 | 66 | export default useMediaQuery; 67 | -------------------------------------------------------------------------------- /src/hook/useMementoState/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useMementoState 4 |

5 |
6 | 7 |
8 | 9 | A React hook that keeps the track of the history of the state changes. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-memento-state?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-memento-state) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-memento-state?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-memento-state) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-memento-state?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-memento-state) 15 | 16 | ```bash 17 | npm i @utilityjs/use-memento-state | yarn add @utilityjs/use-memento-state 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 |

25 | showcase 26 |

27 | 28 |
29 | 30 | ## Usage 31 | 32 | ```tsx 33 | const MementoDemo = () => { 34 | const { 35 | state, 36 | setState, 37 | pastStates, 38 | futureStates, 39 | redo, 40 | undo, 41 | reset 42 | } = useMementoState(0); 43 | 44 | return ( 45 | 46 | 47 | 48 | 49 | 50 |
51 | History 52 |
    53 | {pastStates.map(past => ( 54 |
  • 55 | {past} 56 |
  • 57 | ))} 58 |
  • 59 | {state}{" "} 60 | 61 | ⬅️ 62 | 63 |
  • 64 | {futureStates.map(future => ( 65 |
  • 66 | {future} 67 |
  • 68 | ))} 69 |
70 |
71 | ); 72 | }; 73 | ``` 74 | 75 | ## API 76 | 77 | ### `useMementoState(initialPresent)` 78 | 79 | ```ts 80 | interface Mementos { 81 | state: T; 82 | pastStates: T[]; 83 | futureStates: T[]; 84 | setState: React.Dispatch>; 85 | undo: () => void; 86 | redo: () => void; 87 | reset: () => void; 88 | hasPastState: () => boolean; 89 | hasFutureState: () => boolean; 90 | } 91 | declare const useMementoState: (initialPresent: T) => Mementos; 92 | ``` 93 | 94 | #### `initialPresent` 95 | 96 | The value of the initial present state. 97 | 98 | -------------------------------------------------------------------------------- /src/hook/useMementoState/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useMementoState"; 2 | -------------------------------------------------------------------------------- /src/hook/useMementoState/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-memento-state", 3 | "version": "1.0.1", 4 | "description": "A React hook that keeps the track of the history of the state changes.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/useMementoState" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useMementoState", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "use memento state", 25 | "memento", 26 | "redo state", 27 | "undo state", 28 | "history", 29 | "use state with history" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/hook/useMementoState/use-memento-state-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimshins/utilityjs/310ad1d404711faf578f6ab4a0cfd2fd96a77b83/src/hook/useMementoState/use-memento-state-img.png -------------------------------------------------------------------------------- /src/hook/useMementoState/useMementoState.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type Action = 4 | | { type: "UNDO" } 5 | | { type: "REDO" } 6 | | { type: "RESET"; initialState: State } 7 | | { type: "SET"; nextPresent: React.SetStateAction }; 8 | 9 | type State = { past: T[]; present: T; future: T[] }; 10 | 11 | type Reducer = (prevState: State, action: Action) => State; 12 | 13 | interface Mementos { 14 | state: T; 15 | pastStates: T[]; 16 | futureStates: T[]; 17 | setState: React.Dispatch>; 18 | undo: () => void; 19 | redo: () => void; 20 | reset: () => void; 21 | hasPastState: () => boolean; 22 | hasFutureState: () => boolean; 23 | } 24 | 25 | const reducer = (prevState: State, action: Action) => { 26 | const { past, present, future } = prevState; 27 | 28 | switch (action.type) { 29 | case "UNDO": { 30 | const newPresent = past[past.length - 1]; 31 | const newPast = past.slice(0, past.length - 1); 32 | 33 | return { 34 | past: newPast, 35 | present: newPresent, 36 | future: [present, ...future] 37 | }; 38 | } 39 | case "REDO": { 40 | const newPresent = future[0]; 41 | const newFuture = future.slice(1); 42 | 43 | return { 44 | past: [...past, present], 45 | present: newPresent, 46 | future: newFuture 47 | }; 48 | } 49 | case "SET": { 50 | const { nextPresent } = action; 51 | 52 | if (nextPresent === present) return prevState; 53 | 54 | type SetterFn = (prev: T) => T; 55 | 56 | const newPresent = 57 | typeof nextPresent === "function" 58 | ? (nextPresent as SetterFn)(present) 59 | : nextPresent; 60 | 61 | return { past: [...past, present], present: newPresent, future: [] }; 62 | } 63 | case "RESET": 64 | return action.initialState; 65 | default: 66 | return prevState; 67 | } 68 | }; 69 | 70 | const useMementoState = (initialPresent: T): Mementos => { 71 | const { current: INITIAL_STATE } = React.useRef>({ 72 | past: [], 73 | present: initialPresent, 74 | future: [] 75 | }); 76 | 77 | const [state, dispatch] = React.useReducer>( 78 | reducer, 79 | INITIAL_STATE 80 | ); 81 | 82 | const hasFutureState = React.useCallback( 83 | () => state.future.length > 0, 84 | [state] 85 | ); 86 | const hasPastState = React.useCallback(() => state.past.length > 0, [state]); 87 | 88 | const setState: Mementos["setState"] = React.useCallback( 89 | nextPresent => dispatch({ type: "SET", nextPresent }), 90 | [] 91 | ); 92 | 93 | const undo: Mementos["undo"] = React.useCallback(() => { 94 | if (hasPastState()) dispatch({ type: "UNDO" }); 95 | }, [hasPastState]); 96 | 97 | const redo: Mementos["redo"] = React.useCallback(() => { 98 | if (hasFutureState()) dispatch({ type: "REDO" }); 99 | }, [hasFutureState]); 100 | 101 | const reset: Mementos["reset"] = React.useCallback( 102 | () => dispatch({ type: "RESET", initialState: INITIAL_STATE }), 103 | // eslint-disable-next-line react-hooks/exhaustive-deps 104 | [] 105 | ); 106 | 107 | return { 108 | state: state.present, 109 | pastStates: state.past, 110 | futureStates: state.future, 111 | setState, 112 | hasFutureState, 113 | hasPastState, 114 | undo, 115 | redo, 116 | reset 117 | }; 118 | }; 119 | 120 | export default useMementoState; 121 | -------------------------------------------------------------------------------- /src/hook/useOnChange/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useOnChange 4 |

5 |
6 | 7 |
8 | 9 | A React hook that invokes a callback anytime a value changes. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-on-change?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-on-change) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-on-change?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-on-change) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-on-change?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-on-change) 15 | 16 | ```bash 17 | npm i @utilityjs/use-on-change | yarn add @utilityjs/use-on-change 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```tsx 27 | import useOnChange from "@utilityjs/use-on-change"; 28 | import * as React from "react"; 29 | 30 | const MyComponent = (props) => { 31 | useOnChange(props.isOpen, (currentOpenState) => { 32 | console.log(`The current open state is: ${currentOpenState}`); 33 | }) 34 | 35 | return ...; 36 | }; 37 | ``` 38 | 39 | ## API 40 | 41 | ### `useOnChange(value, onChange)` 42 | 43 | ```ts 44 | declare const useOnChange: (value: T, onChange: (current: T) => void) => void; 45 | ``` 46 | 47 | #### `value` 48 | 49 | The value to listen on. 50 | 51 | #### `onChange` 52 | 53 | The callback that is called when value changes. -------------------------------------------------------------------------------- /src/hook/useOnChange/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useOnChange"; 2 | -------------------------------------------------------------------------------- /src/hook/useOnChange/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-on-change", 3 | "version": "1.0.3", 4 | "description": "A React hook that invokes a callback anytime a value changes.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/useOnChange" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useOnChange", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "dependencies": { 20 | "@utilityjs/use-get-latest": "^1.0.2", 21 | "@utilityjs/use-previous-value": "^1.0.1" 22 | }, 23 | "keywords": [ 24 | "javascript", 25 | "typescript", 26 | "react", 27 | "react hook", 28 | "onchange", 29 | "change", 30 | "use onchange" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /src/hook/useOnChange/useOnChange.ts: -------------------------------------------------------------------------------- 1 | import useGetLatest from "@utilityjs/use-get-latest"; 2 | import usePreviousValue from "@utilityjs/use-previous-value"; 3 | import * as React from "react"; 4 | 5 | const useOnChange = (value: T, onChange: (current: T) => void): void => { 6 | const cachedOnChange = useGetLatest(onChange); 7 | const prevValue = usePreviousValue(value); 8 | 9 | React.useEffect(() => { 10 | if (value !== prevValue) cachedOnChange.current(value); 11 | // eslint-disable-next-line react-hooks/exhaustive-deps 12 | }, [value, prevValue]); 13 | }; 14 | 15 | export default useOnChange; 16 | -------------------------------------------------------------------------------- /src/hook/useOnOutsideClick/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useOnOutsideClick 4 |

5 |
6 | 7 |
8 | 9 | A React hook that invokes a callback when user clicks outside of the target element. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-on-outside-click?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-on-outside-click) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-on-outside-click?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-on-outside-click) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-on-outside-click?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-on-outside-click) 15 | 16 | ```bash 17 | npm i @utilityjs/use-on-outside-click | yarn add @utilityjs/use-on-outside-click 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```tsx 27 | const MyComponent = (props) => { 28 | const targetRef = React.useRef(); 29 | 30 | useOnOutsideClick(targetRef, () => { 31 | console.log("OUTSIDE!"); 32 | }); 33 | 34 | useOnOutsideClick(targetRef, () => { 35 | console.log("OUTSIDE AND NOT #some-id!"); 36 | // Extending the condition 37 | }, event => ((event.target) as Node).id !== "some-id"); 38 | 39 | return ( 40 | 41 |
...
42 |
43 | ); 44 | }; 45 | ``` 46 | 47 | ## API 48 | 49 | ### `useOnOutsideClick(target, callback, extendCondition?)` 50 | 51 | ```ts 52 | declare const useOnOutsideClick: ( 53 | target: T | React.RefObject | null, 54 | callback: (event: MouseEvent) => void, 55 | extendCondition?: (event: MouseEvent) => boolean 56 | ) => void; 57 | ``` 58 | 59 | #### `target` 60 | 61 | The target to test the conditions against. 62 | 63 | #### `callback` 64 | 65 | The callback that is called when the conditions are met. 66 | 67 | #### `extendCondition` 68 | 69 | The function that extends the test conditions. -------------------------------------------------------------------------------- /src/hook/useOnOutsideClick/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useOnOutsideClick"; 2 | -------------------------------------------------------------------------------- /src/hook/useOnOutsideClick/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-on-outside-click", 3 | "version": "1.0.5", 4 | "description": "A React hook that invokes a callback when user clicks outside of the target element.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/useOnOutsideClick" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useOnOutsideClick", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "dependencies": { 20 | "@utilityjs/use-event-listener": "^1.0.6", 21 | "@utilityjs/use-get-latest": "^1.0.2" 22 | }, 23 | "keywords": [ 24 | "javascript", 25 | "typescript", 26 | "react", 27 | "react hook", 28 | "outside click", 29 | "outside click handler", 30 | "on outside click" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /src/hook/useOnOutsideClick/useOnOutsideClick.ts: -------------------------------------------------------------------------------- 1 | import useGetLatest from "@utilityjs/use-get-latest"; 2 | import useEventListener from "@utilityjs/use-event-listener"; 3 | import * as React from "react"; 4 | 5 | const useOnOutsideClick = ( 6 | target: React.RefObject | T | null, 7 | callback: (event: MouseEvent) => void, 8 | extendCondition: (event: MouseEvent) => boolean = () => true 9 | ): void => { 10 | const cachedCallback = useGetLatest(callback); 11 | 12 | useEventListener({ 13 | target: typeof window === "undefined" ? null : document, 14 | eventType: "click", 15 | handler: event => { 16 | const element = target && "current" in target ? target.current : target; 17 | 18 | if ( 19 | element !== null && 20 | element !== event.target && 21 | !element.contains(event.target as Node) && 22 | extendCondition(event) 23 | ) { 24 | cachedCallback.current(event); 25 | } 26 | } 27 | }); 28 | }; 29 | 30 | export default useOnOutsideClick; 31 | -------------------------------------------------------------------------------- /src/hook/usePersistedState/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | usePersistedState 4 |

5 |
6 | 7 |
8 | 9 | A React hook that provides a SSR-friendly multi-tab persistent state.\ 10 | (also supports multiple instances with the same key) 11 | 12 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 13 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-persisted-state?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-persisted-state) 14 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-persisted-state?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-persisted-state) 15 | [![types](https://img.shields.io/npm/types/@utilityjs/use-persisted-state?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-persisted-state) 16 | 17 | ```bash 18 | npm i @utilityjs/use-persisted-state | yarn add @utilityjs/use-persisted-state 19 | ``` 20 | 21 |
22 | 23 |
24 | 25 | ## Usage 26 | 27 | ```tsx 28 | const App: React.FC = () => { 29 | const [state, setState] = usePersistedState(0, { name: "count" }); 30 | 31 | return ( 32 |
33 | 34 |
{state}
35 |
36 | ); 37 | }; 38 | ``` 39 | 40 | ## API 41 | 42 | ### `usePersistedState(initialState, persistOptions)` 43 | 44 | ```ts 45 | type StorageValue = { 46 | state: T; 47 | }; 48 | 49 | interface PersistOptions { 50 | /** Name of the storage (must be unique) */ 51 | name: string; 52 | /** 53 | * A function returning a storage. 54 | * The storage must fit `window.localStorage`'s api. 55 | * 56 | * @default () => localStorage 57 | */ 58 | getStorage?: () => Storage | null; 59 | /** 60 | * Use a custom serializer. 61 | * The returned string will be stored in the storage. 62 | * 63 | * @default JSON.stringify 64 | */ 65 | serializer?: (state: StorageValue) => string; 66 | /** 67 | * Use a custom deserializer. 68 | * Must return an object matching StorageValue 69 | * 70 | * @param str The storage's current value. 71 | * @default JSON.parse 72 | */ 73 | deserializer?: (str: string) => StorageValue; 74 | } 75 | 76 | declare const usePersistedState: ( 77 | initialState: T, 78 | options: PersistOptions 79 | ) => [T, React.Dispatch>]; 80 | ``` 81 | 82 | #### `initialState` 83 | 84 | The initial value of the state. 85 | 86 | #### `persistOptions` 87 | 88 | The options to adjust the persistence behavior. 89 | 90 | ##### `persistOptions.name` 91 | 92 | The name of the storage (must be unique). 93 | 94 | ##### `persistOptions.getStorage` 95 | ###### `default: () => localStorage` 96 | A function returning a storage. The storage must fit `window.localStorage`'s api. 97 | 98 | ##### `persistOptions.serializer` 99 | ###### `default: JSON.stringify` 100 | A custom serializer. The returned string will be stored in the storage. 101 | 102 | ##### `persistOptions.deserializer` 103 | ###### `default: JSON.parse` 104 | A custom deserializer. Must return an object matching `StorageValue` -------------------------------------------------------------------------------- /src/hook/usePersistedState/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./usePersistedState"; 2 | -------------------------------------------------------------------------------- /src/hook/usePersistedState/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-persisted-state", 3 | "version": "1.1.3", 4 | "description": "A React hook that provides a SSR-friendly multi-tab persistent state.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/usePersistedState" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/usePersistedState", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "dependencies": { 20 | "@utilityjs/use-event-listener": "^1.0.6", 21 | "@utilityjs/use-get-latest": "^1.0.2" 22 | }, 23 | "keywords": [ 24 | "javascript", 25 | "typescript", 26 | "react", 27 | "react hook", 28 | "persisted state", 29 | "use persisted state" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/hook/usePreviousValue/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | usePreviousValue 4 |

5 |
6 | 7 |
8 | 9 | A React hook that returns a value from the previous render. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-previous-value?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-previous-value) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-previous-value?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-previous-value) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-previous-value?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-previous-value) 15 | 16 | ```bash 17 | npm i @utilityjs/use-previous-value | yarn add @utilityjs/use-previous-value 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```ts 27 | import usePreviousValue from "@utilityjs/use-previous-value"; 28 | import * as React from "react"; 29 | 30 | const useHook = () => { 31 | const [open, setOpen] = React.useState(false); 32 | 33 | const prevOpen = usePreviousValue(open); 34 | 35 | React.useEffect(() => { 36 | if (open !== prevOpen) { 37 | console.log("The state has been changed!"); 38 | } 39 | }, [open]) 40 | }; 41 | ``` 42 | 43 | ## API 44 | 45 | ### `usePreviousValue(value)` 46 | 47 | ```ts 48 | declare const usePreviousValue: (value: T) => T | undefined; 49 | ``` 50 | 51 | #### `value` 52 | 53 | The value on the current render. -------------------------------------------------------------------------------- /src/hook/usePreviousValue/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./usePreviousValue"; 2 | -------------------------------------------------------------------------------- /src/hook/usePreviousValue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-previous-value", 3 | "version": "1.0.1", 4 | "description": "A React hook that returns a value from the previous render.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/usePreviousValue" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/usePreviousValue", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "previous", 25 | "previous value", 26 | "use previous value" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/hook/usePreviousValue/usePreviousValue.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const usePreviousValue = (value: T): T | undefined => { 4 | const ref = React.useRef(); 5 | 6 | React.useEffect(() => void (ref.current = value), [value]); 7 | 8 | return ref.current; 9 | }; 10 | 11 | export default usePreviousValue; 12 | -------------------------------------------------------------------------------- /src/hook/usePubSub/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | usePubSub 4 |

5 |
6 | 7 |
8 | 9 | A React hook that provides the Publish/Subscribe pattern. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-pub-sub?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-pub-sub) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-pub-sub?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-pub-sub) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-pub-sub?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-pub-sub) 15 | 16 | ```bash 17 | npm i @utilityjs/use-pub-sub | yarn add @utilityjs/use-pub-sub 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```tsx 27 | import * as React from "react"; 28 | import createPubSub from "@utilityjs/use-pub-sub"; 29 | 30 | // With global registry 31 | const { publish, useSubscribe } = createPubSub(); 32 | 33 | // With scoped registry 34 | const { publish, useSubscribe } = createPubSub({} /* A registry object */); 35 | 36 | const SubscriberComponent = () => { 37 | const [value, setValue] = React.useState(0); 38 | 39 | useSubscribe("increase", () => { 40 | console.log("Increase message received!"); 41 | setValue(v => v + 1); 42 | }); 43 | 44 | useSubscribe("decrease", () => { 45 | console.log("Decrease message received!"); 46 | setValue(v => v - 1); 47 | }); 48 | 49 | return

Result: {value}

50 | } 51 | 52 | const PublisherComponent = () => { 53 | return ( 54 | <> 55 | 56 | 57 | 58 | ); 59 | } 60 | 61 | const Page = () => { 62 | return ( 63 |
64 | 65 | 66 |
67 | ); 68 | }; 69 | 70 | export default Page; 71 | ``` 72 | 73 | ## API 74 | 75 | ### `createPubSub(scopedRegistry?)` 76 | 77 | ```ts 78 | export declare type Callback = () => void; 79 | export declare type Registry = Record; 80 | 81 | declare type Unsubscribe = (channel: string, callback: Callback) => void; 82 | declare type Subscribe = (channel: string, callback: Callback) => void; 83 | declare type Publish = (channel: string) => void; 84 | 85 | declare const createPubSub: (scopedRegistry?: Registry) => { 86 | useSubscribe: Subscribe; 87 | unsubscribe: Unsubscribe; 88 | publish: Publish; 89 | }; 90 | ``` 91 | 92 | #### `scopedRegistry` 93 | 94 | The registry object to use instead of the global registry instance. -------------------------------------------------------------------------------- /src/hook/usePubSub/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./usePubSub"; 2 | -------------------------------------------------------------------------------- /src/hook/usePubSub/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-pub-sub", 3 | "version": "1.0.0", 4 | "description": "A React hook that provides the Publish/Subscribe pattern.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/usePubSub" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/usePubSub", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "publish/subscribe", 25 | "pub/sub", 26 | "use pub/sub hook" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/hook/usePubSub/usePubSub.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export type Callback = () => void; 4 | export type Registry = Record; 5 | 6 | type Unsubscribe = (channel: string, callback: Callback) => void; 7 | type Subscribe = (channel: string, callback: Callback) => void; 8 | type Publish = (channel: string) => void; 9 | 10 | const globalRegistry: Registry = {}; 11 | 12 | const makeUnsubscribe = 13 | (registry: Registry) => (channel: string, callback: Callback) => { 14 | if (!registry[channel]) return; 15 | 16 | const cbs = registry[channel]; 17 | registry[channel] = cbs.filter(cb => cb !== callback); 18 | }; 19 | 20 | const makeSubscribe = 21 | (registry: Registry) => (channel: string, callback: Callback) => { 22 | if (!registry[channel]) registry[channel] = []; 23 | 24 | const unsubscriber = () => makeUnsubscribe(registry)(channel, callback); 25 | 26 | const cbs = registry[channel]; 27 | if (cbs.includes(callback)) return unsubscriber; 28 | 29 | registry[channel] = [...cbs, callback]; 30 | 31 | return unsubscriber; 32 | }; 33 | 34 | const makePublish = (registry: Registry) => (channel: string) => { 35 | if (!registry[channel]) return; 36 | 37 | registry[channel].forEach(cb => void cb()); 38 | }; 39 | 40 | export const createPubSub = ( 41 | scopedRegistry?: Registry 42 | ): { 43 | useSubscribe: Subscribe; 44 | unsubscribe: Unsubscribe; 45 | publish: Publish; 46 | } => { 47 | const registry = scopedRegistry ?? globalRegistry; 48 | 49 | const useSubscribe = (channel: string, callback: Callback) => { 50 | React.useEffect( 51 | () => makeSubscribe(registry)(channel, callback), 52 | [channel, callback] 53 | ); 54 | }; 55 | 56 | return { 57 | useSubscribe, 58 | publish: makePublish(registry), 59 | unsubscribe: makeUnsubscribe(registry) 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /src/hook/useRegisterNodeRef/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useRegisterNodeRef 4 |

5 |
6 | 7 |
8 | 9 | A React hook that helps you to run some code when a DOM node mounts/dismounts. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-register-node-ref?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-register-node-ref) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-register-node-ref?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-register-node-ref) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-register-node-ref?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-register-node-ref) 15 | 16 | ```bash 17 | npm i @utilityjs/use-register-node-ref | yarn add @utilityjs/use-register-node-ref 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | Keep in mind that `useRef` doesn't notify you when its content changes. Mutating the `current` property doesn't cause a re-render. This hook will register a callback to DOM node so you can be notified when it mounts/dismounts. 25 | 26 | This hook also supports dynamic/conditional rendering as well. 27 | 28 |
29 | 30 | ## Usage 31 | 32 | ```tsx 33 | const App: React.FC = () => { 34 | const [isVisible, setVisibile] = React.useState(false); 35 | 36 | const registerRef = useRegisterNodeRef(node => { 37 | // The callback effect to invoke when node mounts/dismounts 38 | 39 | const listener = () => { 40 | console.log("clicked"); 41 | }; 42 | 43 | if (node) node.addEventListener("click", listener); 44 | 45 | // The cleanup function 46 | return () => { 47 | if (node) node.removeEventListener("click", listener); 48 | }; 49 | }); 50 | 51 | return ( 52 |
53 | 54 | {isVisible &&
This is the DIV!
} 55 |
56 | ); 57 | }; 58 | ``` 59 | 60 | ## API 61 | 62 | ### `useRegisterNodeRef(callback, deps?)` 63 | 64 | ```ts 65 | declare type Destructor = () => void | undefined; 66 | 67 | declare type Callback = ( 68 | node: T | null 69 | ) => void | Destructor; 70 | 71 | declare const useRegisterNodeRef: ( 72 | callback: Callback, 73 | deps?: React.DependencyList 74 | ) => (node: T | null) => void; 75 | ``` 76 | 77 | #### `callback` 78 | 79 | The callback effect to invoke when DOM node mounts/dismounts.
80 | Note: This callback can also return a cleanup function. 81 | 82 | #### `deps` - (default: `[]`) 83 | 84 | If present, the callback will only be updated if the values in the list change. -------------------------------------------------------------------------------- /src/hook/useRegisterNodeRef/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useRegisterNodeRef"; 2 | -------------------------------------------------------------------------------- /src/hook/useRegisterNodeRef/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-register-node-ref", 3 | "version": "1.1.0", 4 | "description": "A React hook that helps you to run some code when a DOM node mounts/dismounts.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/useRegisterNodeRef" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useRegisterNodeRef", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "ref callback", 25 | "useref callback", 26 | "register dom node" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/hook/useRegisterNodeRef/useRegisterNodeRef.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type Destructor = () => void | undefined; 4 | type Callback = (node: T) => void | Destructor; 5 | 6 | const useRegisterNodeRef = ( 7 | callback: Callback, 8 | deps: React.DependencyList = [] 9 | ): ((node: T | null) => void) => { 10 | const cleanupRef = React.useRef(null); 11 | 12 | const registerRef = React.useCallback( 13 | (node: T | null) => { 14 | if (cleanupRef.current !== null) { 15 | cleanupRef.current(); 16 | cleanupRef.current = null; 17 | } 18 | 19 | if (node) { 20 | const cleanup = callback(node); 21 | 22 | if (cleanup) cleanupRef.current = cleanup; 23 | else cleanupRef.current = null; 24 | } 25 | }, 26 | // eslint-disable-next-line react-hooks/exhaustive-deps 27 | deps 28 | ); 29 | 30 | return registerRef; 31 | }; 32 | 33 | export default useRegisterNodeRef; 34 | -------------------------------------------------------------------------------- /src/hook/useResizeSensor/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useResizeSensor 4 |

5 |
6 | 7 |
8 | 9 | A React hook that handles element resizes using native `ResizeObserver`. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-resize-sensor?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-resize-sensor) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-resize-sensor?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-resize-sensor) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-resize-sensor?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-resize-sensor) 15 | 16 | ```bash 17 | npm i @utilityjs/use-resize-sensor | yarn add @utilityjs/use-resize-sensor 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 |
25 | Live Demo: https://6g30j.csb.app/ 26 |
27 | 28 |
29 | 30 | ## Usage 31 | 32 | ```tsx 33 | import useResizeSensor from "@utilityjs/use-resize-sensor"; 34 | import * as React from "react"; 35 | 36 | const Component = () => { 37 | const { width, height, registerNode } = useResizeSensor(); 38 | 39 | return
Width: {width} | Height: {height}
40 | }; 41 | ``` 42 | 43 | ## API 44 | 45 | ### `useResizeSensor(refreshOptions?)` 46 | 47 | ```ts 48 | interface RefreshOptions { 49 | mode: "debounce" | "throttle"; 50 | rate?: number; 51 | leading?: boolean; 52 | trailing?: boolean; 53 | } 54 | 55 | declare const useResizeSensor: ( 56 | refreshOptions?: RefreshOptions | undefined 57 | ) => { 58 | width: number; 59 | height: number; 60 | registerNode: (node: T | null) => void; 61 | }; 62 | ``` 63 | 64 | #### `refreshOptions` 65 | 66 | The options to adjust the refresh/tick behavior. 67 | 68 | ##### `refreshOptions.mode` 69 | 70 | Values: `throttle`([Documentation](https://lodash.com/docs/4.17.15#throttle)) | `debounce`([Documentation](https://lodash.com/docs/4.17.15#debounce)) | `undefined` 71 | 72 | ##### `refreshOptions.rate` | Default: `250` 73 | 74 | The number of milliseconds to either delay or throttle invocations to. 75 | 76 | ##### `refreshOptions.leading` | Default: (`true` for throttle mode / `false` for debounce mode) 77 | 78 | Specify invoking on the leading edge of the timeout. 79 | 80 | ##### `refreshOptions.trailing` | Default: `true` 81 | 82 | Specify invoking on the trailing edge of the timeout. -------------------------------------------------------------------------------- /src/hook/useResizeSensor/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useResizeSensor"; 2 | -------------------------------------------------------------------------------- /src/hook/useResizeSensor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-resize-sensor", 3 | "version": "1.0.2", 4 | "description": "A React hook that handles element resizes using native `ResizeObserver`.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/hook/useResizeSensor" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useResizeSensor", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "dependencies": { 20 | "lodash.debounce": "^4.0.8", 21 | "lodash.throttle": "^4.1.1", 22 | "@types/lodash.debounce": "^4.0.6", 23 | "@types/lodash.throttle": "^4.1.6" 24 | }, 25 | "keywords": [ 26 | "javascript", 27 | "typescript", 28 | "react", 29 | "react hook", 30 | "resize observer", 31 | "use resize observer", 32 | "use resize sensor", 33 | "resize sensor", 34 | "size sensor", 35 | "size observer" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/hook/useResizeSensor/useResizeSensor.ts: -------------------------------------------------------------------------------- 1 | import debounce from "lodash.debounce"; 2 | import throttle from "lodash.throttle"; 3 | import * as React from "react"; 4 | 5 | interface RefreshOptions { 6 | mode?: "debounce" | "throttle"; 7 | rate?: number; 8 | leading?: boolean; 9 | trailing?: boolean; 10 | } 11 | 12 | interface Return { 13 | width: number; 14 | height: number; 15 | registerNode: (node: T | null) => void; 16 | } 17 | 18 | interface Debounced unknown> { 19 | (...args: Parameters): ReturnType | undefined; 20 | cancel(): void; 21 | flush(): ReturnType | undefined; 22 | } 23 | 24 | const setRefreshRate = ( 25 | cb: ResizeObserverCallback, 26 | refreshOptions?: RefreshOptions 27 | ): Debounced | ResizeObserverCallback => { 28 | const { mode, rate = 250, leading, trailing } = refreshOptions || {}; 29 | 30 | switch (mode) { 31 | case "debounce": { 32 | const lodashOptions = { 33 | leading: leading != null ? leading : false, 34 | trailing: trailing != null ? trailing : true 35 | }; 36 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 37 | return debounce( 38 | cb, 39 | rate, 40 | lodashOptions 41 | ) as Debounced; 42 | } 43 | case "throttle": { 44 | const lodashOptions = { 45 | leading: leading != null ? leading : true, 46 | trailing: trailing != null ? trailing : true 47 | }; 48 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 49 | return throttle( 50 | cb, 51 | rate, 52 | lodashOptions 53 | ) as Debounced; 54 | } 55 | default: 56 | return cb; 57 | } 58 | }; 59 | 60 | const useResizeSensor = (refreshOptions?: RefreshOptions): Return => { 61 | const [size, setSize] = React.useState({ width: 0, height: 0 }); 62 | 63 | const handleOnResize: ResizeObserverCallback = React.useCallback(entries => { 64 | entries.forEach(entry => { 65 | const { width, height } = entry.contentRect; 66 | 67 | setSize(prevSize => { 68 | if (prevSize.width === width && prevSize.height === height) 69 | return prevSize; 70 | 71 | return { width, height }; 72 | }); 73 | }); 74 | }, []); 75 | 76 | const resizeCallback = React.useMemo( 77 | () => setRefreshRate(handleOnResize, refreshOptions), 78 | [handleOnResize, refreshOptions] 79 | ); 80 | 81 | const registerCleanupRef = React.useRef<(() => void) | null>(null); 82 | 83 | const registerNode = React.useCallback( 84 | (node: T | null) => { 85 | if (registerCleanupRef.current !== null) { 86 | registerCleanupRef.current(); 87 | registerCleanupRef.current = null; 88 | } 89 | 90 | if (node != null) { 91 | const observer = new ResizeObserver(resizeCallback); 92 | observer.observe(node); 93 | 94 | registerCleanupRef.current = () => { 95 | observer.disconnect(); 96 | if ((resizeCallback as Debounced).cancel) 97 | (resizeCallback as Debounced).cancel(); 98 | }; 99 | } 100 | }, 101 | [resizeCallback] 102 | ); 103 | 104 | return { width: size.width, height: size.height, registerNode }; 105 | }; 106 | 107 | export default useResizeSensor; 108 | -------------------------------------------------------------------------------- /src/hook/useScrollGuard/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useScrollGuard 4 |

5 |
6 | 7 |
8 | 9 | A React hook that disables/enables the page scroll. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-scroll-guard?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-scroll-guard) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-scroll-guard?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-scroll-guard) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-scroll-guard?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-scroll-guard) 15 | 16 | ```bash 17 | npm i @utilityjs/use-scroll-guard | yarn add @utilityjs/use-scroll-guard 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```tsx 27 | const App = () => { 28 | const { enablePageScroll, disablePageScroll } = useScrollGuard(); 29 | 30 | return ( 31 |
32 | 33 | 34 |
35 | ); 36 | }; 37 | ``` 38 | 39 | ## API 40 | 41 | ### `useScrollGuard()` 42 | 43 | ```ts 44 | declare type UseScrollGuard = () => { 45 | enablePageScroll: () => void; 46 | disablePageScroll: () => void; 47 | }; 48 | 49 | declare const useScrollGuard: UseScrollGuard; 50 | ``` 51 | -------------------------------------------------------------------------------- /src/hook/useScrollGuard/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useScrollGuard"; 2 | -------------------------------------------------------------------------------- /src/hook/useScrollGuard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-scroll-guard", 3 | "version": "1.0.0", 4 | "description": "A React hook that disables/enables the page scroll.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/useScrollGuard" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useScrollGuard", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "dependencies": { 20 | "@utilityjs/use-get-scrollbar-width": "^1.0.1" 21 | }, 22 | "keywords": [ 23 | "javascript", 24 | "typescript", 25 | "react", 26 | "react hook", 27 | "enable page scroll", 28 | "disable page scroll", 29 | "scroll guard" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/hook/useScrollGuard/useScrollGuard.ts: -------------------------------------------------------------------------------- 1 | import useGetScrollbarWidth from "@utilityjs/use-get-scrollbar-width"; 2 | import * as React from "react"; 3 | 4 | type UseScrollGuard = () => { 5 | enablePageScroll: () => void; 6 | disablePageScroll: () => void; 7 | }; 8 | 9 | const useIsomorphicLayoutEffect = 10 | typeof window === "undefined" ? React.useEffect : React.useLayoutEffect; 11 | 12 | const useScrollGuard: UseScrollGuard = () => { 13 | const getScrollbarWidth = useGetScrollbarWidth(); 14 | 15 | const cachedOverflow = React.useRef(""); 16 | const cachedPaddingR = React.useRef(""); 17 | 18 | useIsomorphicLayoutEffect(() => { 19 | cachedOverflow.current = document.body.style.overflow; 20 | cachedPaddingR.current = document.body.style.paddingRight; 21 | }, []); 22 | 23 | const enablePageScroll = React.useCallback(() => { 24 | document.body.style.overflow = cachedOverflow.current; 25 | document.body.style.paddingRight = cachedPaddingR.current; 26 | }, []); 27 | 28 | const disablePageScroll = React.useCallback(() => { 29 | document.body.style.overflow = "hidden"; 30 | document.body.style.paddingRight = `${getScrollbarWidth()}px`; 31 | // eslint-disable-next-line react-hooks/exhaustive-deps 32 | }, []); 33 | 34 | return { enablePageScroll, disablePageScroll }; 35 | }; 36 | 37 | export default useScrollGuard; 38 | -------------------------------------------------------------------------------- /src/hook/useSyncEffect/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | useSyncEffect 4 |

5 |
6 | 7 |
8 | 9 | A React hook that is similar to the `React.useEffect` hook, except it's synchronous and will be called on server as well. 10 | 11 | [![license](https://img.shields.io/github/license/mimshins/utilityjs?color=212121&style=for-the-badge)](https://github.com/mimshins/utilityjs/blob/main/LICENSE) 12 | [![npm latest package](https://img.shields.io/npm/v/@utilityjs/use-sync-effect?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-sync-effect) 13 | [![npm downloads](https://img.shields.io/npm/dm/@utilityjs/use-sync-effect?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-sync-effect) 14 | [![types](https://img.shields.io/npm/types/@utilityjs/use-sync-effect?color=212121&style=for-the-badge)](https://www.npmjs.com/package/@utilityjs/use-sync-effect) 15 | 16 | ```bash 17 | npm i @utilityjs/use-sync-effect | yarn add @utilityjs/use-sync-effect 18 | ``` 19 | 20 |
21 | 22 |
23 | 24 | ## Usage 25 | 26 | ```jsx 27 | import * as React from "react"; 28 | import useSyncEffect from "@utilityjs/use-sync-effect"; 29 | 30 | const MyComponent = (props, ref) => { 31 | const focusState = React.useRef(props.focused); 32 | 33 | // When `props.disabled` changed run the effect synchronously (and also on the first render) 34 | // Look at it as `React.useMemo` hook but instead of returning some value, 35 | // It registers a synchronous effect with a cleanup function. 36 | useSyncEffect(() => { 37 | if (props.disabled && focusState.current) focusState.current = false; 38 | return () => { 39 | // cleanup function 40 | } 41 | }, [props.disabled]); 42 | 43 | // ... the code that immediately benefits from the effect ... 44 | 45 | return ( 46 |
...
47 | ); 48 | }; 49 | ``` 50 | 51 | ## API 52 | 53 | ### `useSyncEffect(effectCallback, dependencyList?)` 54 | 55 | ```ts 56 | declare const useSyncEffect = ( 57 | effectCallback: React.EffectCallback, 58 | dependencyList: React.DependencyList 59 | ) => void; 60 | ``` 61 | 62 | #### `effectCallback` 63 | 64 | The effect function. 65 | 66 | #### `dependencyList` 67 | 68 | The list of the attributes that effect callback depends on. 69 | -------------------------------------------------------------------------------- /src/hook/useSyncEffect/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./useSyncEffect"; 2 | -------------------------------------------------------------------------------- /src/hook/useSyncEffect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@utilityjs/use-sync-effect", 3 | "version": "1.0.2", 4 | "description": "A React hook that is similar to the `React.useEffect` hook, except it's synchronous and will be called on server as well.", 5 | "sideEffects": false, 6 | "module": "./esm/index.js", 7 | "main": "./index.js", 8 | "types": "./index.d.ts", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mimshins/utilityjs.git", 13 | "directory": "src/useSyncEffect" 14 | }, 15 | "homepage": "https://github.com/mimshins/utilityjs/tree/main/src/hook/useSyncEffect", 16 | "peerDependencies": { 17 | "react": ">=16.8" 18 | }, 19 | "keywords": [ 20 | "javascript", 21 | "typescript", 22 | "react", 23 | "react hook", 24 | "sync effect", 25 | "side effect", 26 | "sync side effect", 27 | "use sync side effect" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/hook/useSyncEffect/useSyncEffect.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const useSyncEffect = ( 4 | effectCallback: React.EffectCallback, 5 | dependencyList?: React.DependencyList 6 | ): void => { 7 | const key = React.useRef({}); 8 | const cleanupRef = React.useRef>(); 9 | 10 | // eslint-disable-next-line react-hooks/exhaustive-deps 11 | const currentKey = React.useMemo(() => ({}), dependencyList); 12 | 13 | if (key !== currentKey) { 14 | key.current = currentKey; 15 | cleanupRef.current = effectCallback(); 16 | } 17 | 18 | React.useEffect( 19 | () => () => { 20 | if (cleanupRef.current) cleanupRef.current(); 21 | }, 22 | [currentKey] 23 | ); 24 | }; 25 | 26 | export default useSyncEffect; 27 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "./dist/cjs" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ES5", 5 | "outDir": "./dist/esm", 6 | "lib": ["DOM.Iterable", "DOM", "ES2017"], 7 | "jsx": "react-jsx", 8 | "tsBuildInfoFile": ".tsbuildinfo", 9 | "incremental": true, 10 | "pretty": true, 11 | "esModuleInterop": true, 12 | "moduleResolution": "Node", 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noImplicitOverride": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "exactOptionalPropertyTypes": true, 20 | "strict": true, 21 | "declaration": true, 22 | "skipLibCheck": true, 23 | "resolveJsonModule": true, 24 | "isolatedModules": true, 25 | "allowJs": true, 26 | "types": ["node", "react", "react-dom"], 27 | "paths": { 28 | "@utilityjs/use-get-latest": ["./src/hook/useGetLatest"], 29 | "@utilityjs/use-get-latest/*": ["./src/hook/useGetLatest/*"], 30 | "@utilityjs/use-previous-value": ["./src/hook/usePreviousValue"], 31 | "@utilityjs/use-previous-value/*": ["./src/hook/usePreviousValue/*"], 32 | "@utilityjs/use-event-listener": ["./src/hook/useEventListener"], 33 | "@utilityjs/use-event-listener/*": ["./src/hook/useEventListener/*"], 34 | "@utilityjs/use-get-scrollbar-width": [ 35 | "./src/hook/useGetScrollbarWidth/" 36 | ], 37 | "@utilityjs/use-get-scrollbar-width/*": [ 38 | "./src/hook/useGetScrollbarWidth/*" 39 | ], 40 | "@utilityjs/use-register-node-ref": ["./src/hook/useRegisterNodeRef/"], 41 | "@utilityjs/use-register-node-ref/*": ["./src/hook/useRegisterNodeRef/*"], 42 | "@utilityjs/use-persisted-state": ["./src/hook/usePersistedState/"], 43 | "@utilityjs/use-persisted-state/*": ["./src/hook/usePersistedState/*"], 44 | "@utilityjs/use-media-query": ["./src/hook/useMediaQuery/"], 45 | "@utilityjs/use-media-query/*": ["./src/hook/useMediaQuery/*"], 46 | "@utilityjs/use-lazy-initialized-value": [ 47 | "./src/hook/useLazyInitializedValue/" 48 | ], 49 | "@utilityjs/use-lazy-initialized-value/*": [ 50 | "./src/hook/useLazyInitializedValue/*" 51 | ], 52 | "@utilityjs/comparator": ["./src/data-structure/Comparator"], 53 | "@utilityjs/comparator/*": ["./src/data-structure/Comparator/*"], 54 | "@utilityjs/heap": ["./src/data-structure/Heap"], 55 | "@utilityjs/heap/*": ["./src/data-structure/Heap/*"], 56 | "@utilityjs/min-heap": ["./src/data-structure/MinHeap"], 57 | "@utilityjs/min-heap/*": ["./src/data-structure/MinHeap/*"], 58 | "@utilityjs/linked-list": ["./src/data-structure/LinkedList"], 59 | "@utilityjs/linked-list/*": ["./src/data-structure/LinkedList/*"] 60 | } 61 | }, 62 | "include": ["src"], 63 | "exclude": ["dist", "node_modules"] 64 | } 65 | --------------------------------------------------------------------------------