diff --git a/.editorconfig b/.editorconfig index 2b05b2c9c..96ae55a8b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,16 +1,14 @@ root = true [*] -charset = utf-8 +indent_style = tab +tab_width = 2 end_of_line = lf -indent_size = 2 -indent_style = space +charset = utf-8 +trim_trailing_whitespace = true insert_final_newline = true max_line_length = 100 -tab_width = 2 -trim_trailing_whitespace = true -[*.{md,mdx}] -trim_trailing_whitespace = false -indent_size = unset -max_line_length = 100 +[*.yml] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index 3b734df42..d9f8e2ccb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,3 +7,5 @@ storybook-build CHANGELOG.md !.eslintrc.js !.prettierrc.js +!.storybook +!.github diff --git a/.eslintrc.js b/.eslintrc.js index 890abf8bb..3549a08fb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,65 +1,65 @@ module.exports = { - root: true, + root: true, - parserOptions: { - project: './tsconfig.eslint.json', - extraFileExtensions: ['.md', '.mdx'], - }, + parserOptions: { + project: './tsconfig.eslint.json', + extraFileExtensions: ['.md', '.mdx'], + }, - overrides: [ - { - files: ['*.js'], - extends: ['@react-hookz/eslint-config/base.cjs'], - }, - { - files: ['*.jsx'], - extends: ['@react-hookz/eslint-config/base.cjs', '@react-hookz/eslint-config/react.cjs'], - }, - { - files: ['*.ts'], - extends: ['@react-hookz/eslint-config/typescript.cjs'], - }, - { - files: ['*.tsx'], - extends: [ - '@react-hookz/eslint-config/typescript.cjs', - '@react-hookz/eslint-config/react.cjs', - ], - }, - { - files: ['**/__tests__/**/*.js'], - extends: ['@react-hookz/eslint-config/base.cjs', '@react-hookz/eslint-config/jest.cjs'], - }, - { - files: ['**/__tests__/**/*.jsx'], - extends: [ - '@react-hookz/eslint-config/base.cjs', - '@react-hookz/eslint-config/react.cjs', - '@react-hookz/eslint-config/jest.cjs', - ], - }, - { - files: ['**/__tests__/**/*.ts'], - extends: [ - '@react-hookz/eslint-config/typescript-unsafe.cjs', - '@react-hookz/eslint-config/jest.cjs', - ], - }, - { - files: ['**/__tests__/**/*.tsx'], - extends: [ - '@react-hookz/eslint-config/typescript-unsafe.cjs', - '@react-hookz/eslint-config/react.cjs', - '@react-hookz/eslint-config/jest.cjs', - ], - }, - { - files: ['*.md'], - extends: ['@react-hookz/eslint-config/md.cjs'], - }, - { - files: ['*.mdx'], - extends: ['@react-hookz/eslint-config/mdx.cjs'], - }, - ], + overrides: [ + { + files: ['*.js'], + extends: ['@react-hookz/eslint-config/base.cjs'], + }, + { + files: ['*.jsx'], + extends: ['@react-hookz/eslint-config/base.cjs', '@react-hookz/eslint-config/react.cjs'], + }, + { + files: ['*.ts'], + extends: ['@react-hookz/eslint-config/typescript.cjs'], + }, + { + files: ['*.tsx'], + extends: [ + '@react-hookz/eslint-config/typescript.cjs', + '@react-hookz/eslint-config/react.cjs', + ], + }, + { + files: ['**/__tests__/**/*.js'], + extends: ['@react-hookz/eslint-config/base.cjs', '@react-hookz/eslint-config/jest.cjs'], + }, + { + files: ['**/__tests__/**/*.jsx'], + extends: [ + '@react-hookz/eslint-config/base.cjs', + '@react-hookz/eslint-config/react.cjs', + '@react-hookz/eslint-config/jest.cjs', + ], + }, + { + files: ['**/__tests__/**/*.ts'], + extends: [ + '@react-hookz/eslint-config/typescript-unsafe.cjs', + '@react-hookz/eslint-config/jest.cjs', + ], + }, + { + files: ['**/__tests__/**/*.tsx'], + extends: [ + '@react-hookz/eslint-config/typescript-unsafe.cjs', + '@react-hookz/eslint-config/react.cjs', + '@react-hookz/eslint-config/jest.cjs', + ], + }, + { + files: ['*.md'], + extends: ['@react-hookz/eslint-config/md.cjs'], + }, + { + files: ['*.mdx'], + extends: ['@react-hookz/eslint-config/mdx.cjs'], + }, + ], }; diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 5b8ce70c8..8edfff0df 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -30,10 +30,10 @@ What should `@react-hookz/web` be doing? ### Environment Details -- _`@react-hookz/web` version:_ -- _`react` version:_ -- _`react-dom` version:_ -- _`typescript` version:_ -- _OS:_ -- _Browser:_ -- _Did this work in previous versions?_ +- _`@react-hookz/web` version:_ +- _`react` version:_ +- _`react-dom` version:_ +- _`typescript` version:_ +- _OS:_ +- _Browser:_ +- _Did this work in previous versions?_ diff --git a/.storybook/main.js b/.storybook/main.js index 2db515234..caa285adc 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -1,25 +1,25 @@ module.exports = { - core: { - builder: 'webpack5', - }, - stories: ['../src/**/__docs__/*.mdx', '../src/**/__docs__/*.tsx'], - addons: [ - '@storybook/addon-postcss', - '@storybook/addon-links', - { - name: '@storybook/addon-essentials', - options: { - backgrounds: false, - }, - }, - ], - reactOptions: { - fastRefresh: true, - }, - staticDirs: ['./public'], - managerWebpack(config, options) { - options.cache.set = () => Promise.resolve(); + core: { + builder: 'webpack5', + }, + stories: ['../src/**/__docs__/*.mdx', '../src/**/__docs__/*.tsx'], + addons: [ + '@storybook/addon-postcss', + '@storybook/addon-links', + { + name: '@storybook/addon-essentials', + options: { + backgrounds: false, + }, + }, + ], + reactOptions: { + fastRefresh: true, + }, + staticDirs: ['./public'], + managerWebpack(config, options) { + options.cache.set = () => Promise.resolve(); - return config; - }, + return config; + }, }; diff --git a/.storybook/manager.js b/.storybook/manager.js index 95b959f79..850da4d39 100644 --- a/.storybook/manager.js +++ b/.storybook/manager.js @@ -2,10 +2,10 @@ import { addons } from '@storybook/addons'; import { themes } from '@storybook/theming'; addons.setConfig({ - theme: { - ...themes.light, - brandTitle: '@react-hookz/web', - brandImage: './logo.png', - fontBase: '"Manrope", sans-serif', - }, + theme: { + ...themes.light, + brandTitle: '@react-hookz/web', + brandImage: './logo.png', + fontBase: '"Manrope", sans-serif', + }, }); diff --git a/.storybook/preview.js b/.storybook/preview.js index 6113d1280..257aa40c7 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,4 +1,4 @@ export const parameters = { - layout: 'centered', - viewMode: 'docs', + layout: 'centered', + viewMode: 'docs', }; diff --git a/commitlint.config.js b/commitlint.config.js index 705b8f460..9ca4e70c4 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,6 +1,6 @@ module.exports = { - extends: ['@commitlint/config-conventional'], - rules: { - 'footer-max-line-length': [1, 'always', 100], - }, + extends: ['@commitlint/config-conventional'], + rules: { + 'footer-max-line-length': [1, 'always', 100], + }, }; diff --git a/jest.config.ts b/jest.config.ts index a0636f681..de9b6e151 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,44 +1,44 @@ import { type Config } from 'jest'; const cfg: Config = { - projects: [ - { - displayName: 'dom', - transform: { - '\\.[jt]sx?$': '@swc/jest', - }, - testEnvironment: 'jsdom', - testMatch: ['/src/**/__tests__/dom.[jt]s?(x)'], - setupFiles: ['./src/__tests__/setup.ts'], - }, + projects: [ + { + displayName: 'dom', + transform: { + '\\.[jt]sx?$': '@swc/jest', + }, + testEnvironment: 'jsdom', + testMatch: ['/src/**/__tests__/dom.[jt]s?(x)'], + setupFiles: ['./src/__tests__/setup.ts'], + }, - { - displayName: 'ssr', - transform: { - '\\.[jt]sx?$': '@swc/jest', - }, - testEnvironment: 'node', - testMatch: ['/src/**/__tests__/ssr.[jt]s?(x)'], - }, + { + displayName: 'ssr', + transform: { + '\\.[jt]sx?$': '@swc/jest', + }, + testEnvironment: 'node', + testMatch: ['/src/**/__tests__/ssr.[jt]s?(x)'], + }, - // Needed for output bundle testing - { - displayName: 'dom-package', - transformIgnorePatterns: [], - transform: { - '\\.[jt]sx?$': '@swc/jest', - }, - testEnvironment: 'jsdom', - testMatch: ['/src/**/__tests__/dom.[jt]s?(x)'], - setupFiles: ['./src/__tests__/setup.ts'], - moduleNameMapper: { - '^../..$': '', - }, - }, - ], - collectCoverage: false, - coverageDirectory: './coverage', - collectCoverageFrom: ['./src/**/*.{ts,js,tsx,jsx}', '!**/__tests__/**', '!**/__docs__/**'], + // Needed for output bundle testing + { + displayName: 'dom-package', + transformIgnorePatterns: [], + transform: { + '\\.[jt]sx?$': '@swc/jest', + }, + testEnvironment: 'jsdom', + testMatch: ['/src/**/__tests__/dom.[jt]s?(x)'], + setupFiles: ['./src/__tests__/setup.ts'], + moduleNameMapper: { + '^../..$': '', + }, + }, + ], + collectCoverage: false, + coverageDirectory: './coverage', + collectCoverageFrom: ['./src/**/*.{ts,js,tsx,jsx}', '!**/__tests__/**', '!**/__docs__/**'], }; export default cfg; diff --git a/package.json b/package.json index a86c82dc7..e3461cfaf 100644 --- a/package.json +++ b/package.json @@ -1,125 +1,125 @@ { - "name": "@react-hookz/web", - "version": "23.1.0", - "description": "React hooks done right, for browser and SSR.", - "keywords": [ - "react", - "hook", - "react-hook", - "browser", - "ssr" - ], - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/react-hookz/web.git" - }, - "bugs": { - "url": "https://github.com/react-hookz/web/issues" - }, - "publishConfig": { - "access": "public" - }, - "files": [ - "cjs", - "esm" - ], - "main": "./cjs/index.js", - "types": "./cjs/index.d.ts", - "module": "./esm/index.js", - "sideEffects": false, - "scripts": { - "prepare": "husky install", - "commit": "git-cz", - "build": "yarn build:cleanup && concurrently yarn:build:cjs yarn:build:esm", - "build:cleanup": "rimraf ./dist", - "build:cjs": "ttsc -p ./tsconfig.build.json --module CommonJS --outDir ./cjs", - "build:esm": "ttsc -p ./tsconfig.build.json --module ESNext --outDir ./esm", - "new-hook": "node ./utility/add-new-hook.js", - "test": "jest --selectProjects dom ssr", - "test:coverage": "yarn test --coverage", - "lint": "eslint ./ ./.storybook --ext ts,js,tsx,jsx,md,mdx", - "lint:fix": "yarn lint --fix", - "storybook:watch": "start-storybook -p 6006 --docs --no-manager-cache", - "storybook:build": "build-storybook --docs -o ./storybook-build", - "storybook:deploy": "storybook-to-ghpages -s storybook:build" - }, - "config": { - "commitizen": { - "path": "@commitlint/cz-commitlint" - } - }, - "lint-staged": { - "*.{js,jsx,ts,tsx,md}": "eslint --fix" - }, - "release": { - "plugins": [ - "@semantic-release/commit-analyzer", - "@semantic-release/release-notes-generator", - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/git", - "@semantic-release/github" - ] - }, - "dependencies": { - "@react-hookz/deep-equal": "^1.0.4" - }, - "peerDependencies": { - "js-cookie": "^3.0.5", - "react": "^16.8 || ^17 || ^18", - "react-dom": "^16.8 || ^17 || ^18" - }, - "peerDependenciesMeta": { - "js-cookie": { - "optional": true - } - }, - "devDependencies": { - "@babel/core": "^7.22.5", - "@commitlint/cli": "^17.6.6", - "@commitlint/config-conventional": "^17.6.6", - "@commitlint/cz-commitlint": "^17.5.0", - "@jamesacarr/jest-reporter-github-actions": "^0.0.4", - "@react-hookz/eslint-config": "^2.0.0", - "@react-hookz/eslint-formatter-gha": "^1.0.1", - "@semantic-release/changelog": "^6.0.3", - "@semantic-release/git": "^10.0.1", - "@semantic-release/github": "^9.0.3", - "@storybook/addon-docs": "^6.5.16", - "@storybook/addon-essentials": "^6.5.16", - "@storybook/addon-links": "^6.5.16", - "@storybook/addon-postcss": "^2.0.0", - "@storybook/addons": "^6.5.16", - "@storybook/builder-webpack5": "^6.5.16", - "@storybook/manager-webpack5": "^6.5.16", - "@storybook/react": "^6.5.16", - "@storybook/storybook-deployer": "^2.8.16", - "@storybook/theming": "^6.5.16", - "@swc/core": "^1.3.67", - "@swc/jest": "^0.2.26", - "@testing-library/react-hooks": "^8.0.1", - "@types/jest": "^29.5.2", - "@types/js-cookie": "^3.0.3", - "@types/react": "^18.2.14", - "@types/react-dom": "^18.2.6", - "babel-loader": "^9.1.2", - "commitizen": "^4.3.0", - "commitlint": "^17.6.6", - "concurrently": "^8.2.0", - "husky": "^8.0.3", - "jest": "^29.5.0", - "jest-environment-jsdom": "^29.5.0", - "js-cookie": "^3.0.5", - "lint-staged": "^13.2.3", - "prettier": "^2.8.8", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "rimraf": "^5.0.1", - "semantic-release": "^21.0.6", - "ts-node": "^10.9.1", - "ttypescript": "^1.5.15", - "typescript": "^4.9.5", - "yarn": "^1.22.19" - } + "name": "@react-hookz/web", + "version": "23.1.0", + "description": "React hooks done right, for browser and SSR.", + "keywords": [ + "react", + "hook", + "react-hook", + "browser", + "ssr" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/react-hookz/web.git" + }, + "bugs": { + "url": "https://github.com/react-hookz/web/issues" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "cjs", + "esm" + ], + "main": "./cjs/index.js", + "types": "./cjs/index.d.ts", + "module": "./esm/index.js", + "sideEffects": false, + "scripts": { + "prepare": "husky install", + "commit": "git-cz", + "build": "yarn build:cleanup && concurrently yarn:build:cjs yarn:build:esm", + "build:cleanup": "rimraf ./dist", + "build:cjs": "ttsc -p ./tsconfig.build.json --module CommonJS --outDir ./cjs", + "build:esm": "ttsc -p ./tsconfig.build.json --module ESNext --outDir ./esm", + "new-hook": "node ./utility/add-new-hook.js", + "test": "jest --selectProjects dom ssr", + "test:coverage": "yarn test --coverage", + "lint": "eslint .", + "lint:fix": "yarn lint --fix .", + "storybook:watch": "start-storybook -p 6006 --docs --no-manager-cache", + "storybook:build": "build-storybook --docs -o ./storybook-build", + "storybook:deploy": "storybook-to-ghpages -s storybook:build" + }, + "config": { + "commitizen": { + "path": "@commitlint/cz-commitlint" + } + }, + "lint-staged": { + "*.{js,jsx,ts,tsx,md,mdx}": "eslint --fix" + }, + "release": { + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/git", + "@semantic-release/github" + ] + }, + "dependencies": { + "@react-hookz/deep-equal": "^1.0.4" + }, + "peerDependencies": { + "js-cookie": "^3.0.5", + "react": "^16.8 || ^17 || ^18", + "react-dom": "^16.8 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "js-cookie": { + "optional": true + } + }, + "devDependencies": { + "@babel/core": "^7.22.5", + "@commitlint/cli": "^17.6.6", + "@commitlint/config-conventional": "^17.6.6", + "@commitlint/cz-commitlint": "^17.5.0", + "@jamesacarr/jest-reporter-github-actions": "^0.0.4", + "@react-hookz/eslint-config": "^2.0.0", + "@react-hookz/eslint-formatter-gha": "^1.0.1", + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/git": "^10.0.1", + "@semantic-release/github": "^9.0.3", + "@storybook/addon-docs": "^6.5.16", + "@storybook/addon-essentials": "^6.5.16", + "@storybook/addon-links": "^6.5.16", + "@storybook/addon-postcss": "^2.0.0", + "@storybook/addons": "^6.5.16", + "@storybook/builder-webpack5": "^6.5.16", + "@storybook/manager-webpack5": "^6.5.16", + "@storybook/react": "^6.5.16", + "@storybook/storybook-deployer": "^2.8.16", + "@storybook/theming": "^6.5.16", + "@swc/core": "^1.3.67", + "@swc/jest": "^0.2.26", + "@testing-library/react-hooks": "^8.0.1", + "@types/jest": "^29.5.2", + "@types/js-cookie": "^3.0.3", + "@types/react": "^18.2.14", + "@types/react-dom": "^18.2.6", + "babel-loader": "^9.1.2", + "commitizen": "^4.3.0", + "commitlint": "^17.6.6", + "concurrently": "^8.2.0", + "husky": "^8.0.3", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "js-cookie": "^3.0.5", + "lint-staged": "^13.2.3", + "prettier": "^2.8.8", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "rimraf": "^5.0.1", + "semantic-release": "^21.0.6", + "ts-node": "^10.9.1", + "ttypescript": "^1.5.15", + "typescript": "^4.9.5", + "yarn": "^1.22.19" + } } diff --git a/src/__docs__/ImportPath.tsx b/src/__docs__/ImportPath.tsx index 68d5cb1ef..4ee5c8e3e 100644 --- a/src/__docs__/ImportPath.tsx +++ b/src/__docs__/ImportPath.tsx @@ -2,26 +2,26 @@ import { DocsContext, Source } from '@storybook/addon-docs'; import React, { type FC, useContext } from 'react'; export type ImportPathProps = { - isRoot?: boolean; - isDirect?: boolean; + isRoot?: boolean; + isDirect?: boolean; }; export function ImportPath({ isRoot = true, isDirect = true }: ImportPathProps) { - const context = useContext(DocsContext); + const context = useContext(DocsContext); - const componentName = context.title?.split('/')[1] || 'UnknownComponent'; + const componentName = context.title?.split('/')[1] || 'UnknownComponent'; - const imports: string[] = []; + const imports: string[] = []; - if (isRoot) { - imports.push(`// root import\nimport { ${componentName} } from '@react-hookz/web';`); - } + if (isRoot) { + imports.push(`// root import\nimport { ${componentName} } from '@react-hookz/web';`); + } - if (isDirect) { - imports.push( - `// direct import\nimport { ${componentName} } from '@react-hookz/web/esm/${componentName}';` - ); - } + if (isDirect) { + imports.push( + `// direct import\nimport { ${componentName} } from '@react-hookz/web/esm/${componentName}';` + ); + } - return ; + return ; } diff --git a/src/__docs__/migrating-from-react-use.story.mdx b/src/__docs__/migrating-from-react-use.story.mdx index 3c3c2bec2..5ad1b9ecb 100644 --- a/src/__docs__/migrating-from-react-use.story.mdx +++ b/src/__docs__/migrating-from-react-use.story.mdx @@ -58,9 +58,9 @@ OLD in `react-use`: ```javascript const intersection = useIntersection(elementRef, { - root: rootRef, - rootMargin: '0px', - threshold: 1, + root: rootRef, + rootMargin: '0px', + threshold: 1, }); ``` @@ -68,9 +68,9 @@ NEW in `@react-hookz/web`: ```javascript const intersection = useIntersectionObserver(elementRef, { - root: rootRef, - rootMargin: '0px', - threshold: [0, 0.5], + root: rootRef, + rootMargin: '0px', + threshold: [0, 0.5], }); ``` @@ -267,18 +267,18 @@ In order to replicate the old functionality, you can do the following: const isReady = (React.useRef < boolean) | (null > false); const [cancel, reset] = useTimeoutEffect(() => { - isReady.current = true; - console.log('Hello world'); + isReady.current = true; + console.log('Hello world'); }, 123); const clear = () => { - isReady.current = null; - cancel(); + isReady.current = null; + cancel(); }; const set = () => { - isReady.current = false; - reset(); + isReady.current = false; + reset(); }; // Use set and clear, same as in `react-use`. @@ -304,9 +304,9 @@ OLD in `react-use`: ```javascript const { loading, value, error } = useAsync(async () => { - const response = await fetch(url); - const result = await response.text(); - return result; + const response = await fetch(url); + const result = await response.text(); + return result; }, [url]); console.log(loading); @@ -318,9 +318,9 @@ NEW in `@react-hookz/web`: ```javascript const [{ status, result, error }, { execute }] = useAsync(async () => { - const response = await fetch(url); - const result = await response.text(); - return result; + const response = await fetch(url); + const result = await response.text(); + return result; }); useMountEffect(execute); @@ -338,9 +338,9 @@ OLD in `react-use`: ```javascript const [{ loading, value, error }, doFetch] = useAsync(async () => { - const response = await fetch(url); - const result = await response.text(); - return result; + const response = await fetch(url); + const result = await response.text(); + return result; }, [url]); doFetch(); @@ -350,9 +350,9 @@ NEW in `@react-hookz/web`: ```javascript const [{ status, result, error }, { execute }] = useAsync(async () => { - const response = await fetch(url); - const result = await response.text(); - return result; + const response = await fetch(url); + const result = await response.text(); + return result; }); execute(); @@ -366,9 +366,9 @@ OLD in `react-use`: ```javascript const { loading, value, error, retry } = useAsync(async () => { - const response = await fetch(url); - const result = await response.text(); - return result; + const response = await fetch(url); + const result = await response.text(); + return result; }, [url]); retry(); @@ -378,9 +378,9 @@ NEW in `@react-hookz/web`: ```javascript const [{ status, result, error }, { execute }] = useAsync(async () => { - const response = await fetch(url); - const result = await response.text(); - return result; + const response = await fetch(url); + const result = await response.text(); + return result; }); execute(); @@ -392,7 +392,7 @@ No plans to implement. Use [useEventListener](/docs/dom-useeventlistener--exampl ```javascript useEventListener(window, 'beforeunload', () => { - /* do your stuff here */ + /* do your stuff here */ }); ``` @@ -510,12 +510,12 @@ OLD in `react-use`: ```javascript useEvent( - 'mousemove', - () => { - setState(new Date()); - }, - window, - { passive: true } + 'mousemove', + () => { + setState(new Date()); + }, + window, + { passive: true } ); ``` @@ -523,12 +523,12 @@ NEW in `@react-hookz/web`: ```javascript useEventListener( - window, - 'mousemove', - () => { - setState(new Date()); - }, - { passive: true } + window, + 'mousemove', + () => { + setState(new Date()); + }, + { passive: true } ); ``` @@ -617,8 +617,8 @@ so: const initialValue = 'world'; const defaultValue = 'you'; const [greeting, setGreeting] = useMediatedState( - initialValue, - (newValue) => newValue ?? defaultValue + initialValue, + (newValue) => newValue ?? defaultValue ); console.log(`Hello ${greeting}`); ``` @@ -715,7 +715,7 @@ OLD in `react-use`: ```javascript const [map, { set, remove, reset, setAll }] = useMap({ - hello: 'there', + hello: 'there', }); console.log(JSON.stringify(map, null, 2)); diff --git a/src/__tests__/navigator.vibrate.ts b/src/__tests__/navigator.vibrate.ts index 7be040d62..2ba257138 100644 --- a/src/__tests__/navigator.vibrate.ts +++ b/src/__tests__/navigator.vibrate.ts @@ -1,3 +1,3 @@ export const setupNavigatorVibrate = () => { - navigator.vibrate = (() => true) as typeof navigator.vibrate; + navigator.vibrate = (() => true) as typeof navigator.vibrate; }; diff --git a/src/types.ts b/src/types.ts index 68d72e16b..aadd5e829 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,8 @@ import { type DependencyList } from 'react'; export type DependenciesComparator = ( - a: Deps, - b: Deps + a: Deps, + b: Deps ) => boolean; export type Predicate = (prev: any, next: any) => boolean; @@ -10,5 +10,5 @@ export type Predicate = (prev: any, next: any) => boolean; export type ConditionsList = readonly any[]; export type ConditionsPredicate = ( - conditions: Cond + conditions: Cond ) => boolean; diff --git a/src/useAsync/__docs__/example.stories.tsx b/src/useAsync/__docs__/example.stories.tsx index 9e7a66d9b..47ee790a4 100644 --- a/src/useAsync/__docs__/example.stories.tsx +++ b/src/useAsync/__docs__/example.stories.tsx @@ -2,46 +2,46 @@ import * as React from 'react'; import { useAsync, useMountEffect } from '../..'; export function Example() { - const [state, actions] = useAsync( - () => - new Promise((resolve) => { - setTimeout(() => { - resolve('react-hookz is awesome!'); - }, 3000); - }), - 'react-hookz is' - ); + const [state, actions] = useAsync( + () => + new Promise((resolve) => { + setTimeout(() => { + resolve('react-hookz is awesome!'); + }, 3000); + }), + 'react-hookz is' + ); - useMountEffect(actions.execute); + useMountEffect(actions.execute); - return ( -
-
- Async function will resolve after 3 seconds of wait -
-
-
promise status: {state.status}
-
current value: {state.result ?? 'undefined'}
-
-
- {' '} - -
-
- ); + return ( +
+
+ Async function will resolve after 3 seconds of wait +
+
+
promise status: {state.status}
+
current value: {state.result ?? 'undefined'}
+
+
+ {' '} + +
+
+ ); } diff --git a/src/useAsync/__docs__/story.mdx b/src/useAsync/__docs__/story.mdx index 13dcd7dd3..63147beac 100644 --- a/src/useAsync/__docs__/story.mdx +++ b/src/useAsync/__docs__/story.mdx @@ -17,15 +17,15 @@ Tracks the result and errors of the provided async function and provides handles #### Example - + ## Reference ```ts export function useAsync( - asyncFn: (...params: Args) => Promise, - initialValue?: Result + asyncFn: (...params: Args) => Promise, + initialValue?: Result ): [AsyncState, UseAsyncActions, UseAsyncMeta]; ``` diff --git a/src/useAsync/__tests__/dom.ts b/src/useAsync/__tests__/dom.ts index 2d88bb6a3..07f7e2597 100644 --- a/src/useAsync/__tests__/dom.ts +++ b/src/useAsync/__tests__/dom.ts @@ -2,250 +2,250 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { useAsync } from '../..'; describe('useAsync', () => { - function getControllableAsync() { - const resolve: { current: undefined | ((result: Res) => void) } = { current: undefined }; - const reject: { current: undefined | ((err: Error) => void) } = { current: undefined }; - - return [ - jest.fn( - (..._args: Args) => - // eslint-disable-next-line promise/param-names - new Promise((res, rej) => { - resolve.current = res; - reject.current = rej; - }) - ), - resolve, - reject, - ] as const; - } - - it('should be defined', () => { - expect(useAsync).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useAsync(async () => true)); - expect(result.error).toBeUndefined(); - }); - - it('should not invoke async function on mount if `skipMount` option is passed', () => { - const spy = jest.fn(async () => {}); - renderHook(() => useAsync(spy)); - - expect(spy).not.toHaveBeenCalled(); - }); - - it('should apply `initialValue` arg', async () => { - await act(async () => { - const [spy, resolve] = getControllableAsync(); - const { result } = renderHook(() => useAsync(spy, 3)); - - expect((result.all[0] as ReturnType)[0]).toStrictEqual({ - status: 'not-executed', - error: undefined, - result: 3, - }); - - if (resolve.current) { - resolve.current(2); - } - }); - }); - - it('should have `not-executed` status initially', async () => { - await act(async () => { - const [spy, resolve] = getControllableAsync(); - const { result } = renderHook(() => useAsync(spy)); - - expect(result.current[0]).toStrictEqual({ - status: 'not-executed', - error: undefined, - result: undefined, - }); - - if (resolve.current) { - resolve.current(); - } - }); - }); - - it('should have `loading` status while promise invoked but not resolved', async () => { - const [spy, resolve] = getControllableAsync(); - const { result } = renderHook(() => useAsync(spy)); - - expect(result.current[0]).toStrictEqual({ - status: 'not-executed', - error: undefined, - result: undefined, - }); - - await act(async () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - result.current[1].execute(); - }); - - expect(result.current[0]).toStrictEqual({ - status: 'loading', - error: undefined, - result: undefined, - }); - - await act(async () => { - if (resolve.current) { - resolve.current(); - } - }); - }); - - it('should set `success` status and store `result` state field on fulfill', async () => { - const [spy, resolve] = getControllableAsync(); - const { result } = renderHook(() => useAsync(spy)); - - expect(result.current[0]).toStrictEqual({ - status: 'not-executed', - error: undefined, - result: undefined, - }); - - await act(async () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - result.current[1].execute(); - - if (resolve.current) resolve.current(123); - }); - - expect(result.current[0]).toStrictEqual({ - status: 'success', - error: undefined, - result: 123, - }); - }); - - it('should set `error` status and store `error` state field on reject', async () => { - const [spy, , reject] = getControllableAsync(); - const { result } = renderHook(() => useAsync(spy)); - - expect(result.current[0]).toStrictEqual({ - status: 'not-executed', - error: undefined, - result: undefined, - }); - - const err = new Error('some error'); - - await act(async () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - result.current[1].execute(); - - if (reject.current) reject.current(err); - }); - - expect(result.current[0]).toStrictEqual({ - status: 'error', - error: err, - result: undefined, - }); - }); - - it('should rollback state to initial on `reset` method call', async () => { - const [spy, resolve] = getControllableAsync(); - const { result } = renderHook(() => useAsync(spy, 42)); - - expect(result.current[0]).toStrictEqual({ - status: 'not-executed', - error: undefined, - result: 42, - }); - - await act(async () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - result.current[1].execute(); - - if (resolve.current) resolve.current(1); - }); - - expect(result.current[0]).toStrictEqual({ - status: 'success', - error: undefined, - result: 1, - }); - - await act(async () => { - result.current[1].reset(); - }); - - expect(result.current[0]).toStrictEqual({ - status: 'not-executed', - error: undefined, - result: 42, - }); - }); - - it('should not process results of promise if another was executed', async () => { - const [spy, resolve] = getControllableAsync(); - const { result } = renderHook(() => useAsync(spy, 42)); - - await act(async () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - result.current[1].execute(); - }); - const resolve1 = resolve.current; - - await act(async () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - result.current[1].execute(); - }); - const resolve2 = resolve.current; - - await act(async () => { - if (resolve1) resolve1(1); - if (resolve2) resolve2(2); - }); - - expect(result.current[0]).toStrictEqual({ - status: 'success', - error: undefined, - result: 2, - }); - }); - - it('should not process error of promise if another was executed', async () => { - const [spy, resolve, reject] = getControllableAsync(); - const { result } = renderHook(() => useAsync(spy, 42)); - - await act(async () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - result.current[1].execute(); - }); - const reject1 = reject.current; - - await act(async () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - result.current[1].execute(); - }); - const resolve2 = resolve.current; - - await act(async () => { - if (reject1) reject1(new Error('some err')); - if (resolve2) resolve2(2); - }); - - expect(result.current[0]).toStrictEqual({ - status: 'success', - error: undefined, - result: 2, - }); - }); - - it('should not change methods between renders', () => { - const spy = jest.fn(async () => {}); - const { rerender, result } = renderHook(() => useAsync(spy)); - - const res1 = result.current; - rerender(); - - expect(res1[1].execute).toBe(result.current[1].execute); - expect(res1[1].reset).toBe(result.current[1].reset); - }); + function getControllableAsync() { + const resolve: { current: undefined | ((result: Res) => void) } = { current: undefined }; + const reject: { current: undefined | ((err: Error) => void) } = { current: undefined }; + + return [ + jest.fn( + (..._args: Args) => + // eslint-disable-next-line promise/param-names + new Promise((res, rej) => { + resolve.current = res; + reject.current = rej; + }) + ), + resolve, + reject, + ] as const; + } + + it('should be defined', () => { + expect(useAsync).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useAsync(async () => true)); + expect(result.error).toBeUndefined(); + }); + + it('should not invoke async function on mount if `skipMount` option is passed', () => { + const spy = jest.fn(async () => {}); + renderHook(() => useAsync(spy)); + + expect(spy).not.toHaveBeenCalled(); + }); + + it('should apply `initialValue` arg', async () => { + await act(async () => { + const [spy, resolve] = getControllableAsync(); + const { result } = renderHook(() => useAsync(spy, 3)); + + expect((result.all[0] as ReturnType)[0]).toStrictEqual({ + status: 'not-executed', + error: undefined, + result: 3, + }); + + if (resolve.current) { + resolve.current(2); + } + }); + }); + + it('should have `not-executed` status initially', async () => { + await act(async () => { + const [spy, resolve] = getControllableAsync(); + const { result } = renderHook(() => useAsync(spy)); + + expect(result.current[0]).toStrictEqual({ + status: 'not-executed', + error: undefined, + result: undefined, + }); + + if (resolve.current) { + resolve.current(); + } + }); + }); + + it('should have `loading` status while promise invoked but not resolved', async () => { + const [spy, resolve] = getControllableAsync(); + const { result } = renderHook(() => useAsync(spy)); + + expect(result.current[0]).toStrictEqual({ + status: 'not-executed', + error: undefined, + result: undefined, + }); + + await act(async () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + result.current[1].execute(); + }); + + expect(result.current[0]).toStrictEqual({ + status: 'loading', + error: undefined, + result: undefined, + }); + + await act(async () => { + if (resolve.current) { + resolve.current(); + } + }); + }); + + it('should set `success` status and store `result` state field on fulfill', async () => { + const [spy, resolve] = getControllableAsync(); + const { result } = renderHook(() => useAsync(spy)); + + expect(result.current[0]).toStrictEqual({ + status: 'not-executed', + error: undefined, + result: undefined, + }); + + await act(async () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + result.current[1].execute(); + + if (resolve.current) resolve.current(123); + }); + + expect(result.current[0]).toStrictEqual({ + status: 'success', + error: undefined, + result: 123, + }); + }); + + it('should set `error` status and store `error` state field on reject', async () => { + const [spy, , reject] = getControllableAsync(); + const { result } = renderHook(() => useAsync(spy)); + + expect(result.current[0]).toStrictEqual({ + status: 'not-executed', + error: undefined, + result: undefined, + }); + + const err = new Error('some error'); + + await act(async () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + result.current[1].execute(); + + if (reject.current) reject.current(err); + }); + + expect(result.current[0]).toStrictEqual({ + status: 'error', + error: err, + result: undefined, + }); + }); + + it('should rollback state to initial on `reset` method call', async () => { + const [spy, resolve] = getControllableAsync(); + const { result } = renderHook(() => useAsync(spy, 42)); + + expect(result.current[0]).toStrictEqual({ + status: 'not-executed', + error: undefined, + result: 42, + }); + + await act(async () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + result.current[1].execute(); + + if (resolve.current) resolve.current(1); + }); + + expect(result.current[0]).toStrictEqual({ + status: 'success', + error: undefined, + result: 1, + }); + + await act(async () => { + result.current[1].reset(); + }); + + expect(result.current[0]).toStrictEqual({ + status: 'not-executed', + error: undefined, + result: 42, + }); + }); + + it('should not process results of promise if another was executed', async () => { + const [spy, resolve] = getControllableAsync(); + const { result } = renderHook(() => useAsync(spy, 42)); + + await act(async () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + result.current[1].execute(); + }); + const resolve1 = resolve.current; + + await act(async () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + result.current[1].execute(); + }); + const resolve2 = resolve.current; + + await act(async () => { + if (resolve1) resolve1(1); + if (resolve2) resolve2(2); + }); + + expect(result.current[0]).toStrictEqual({ + status: 'success', + error: undefined, + result: 2, + }); + }); + + it('should not process error of promise if another was executed', async () => { + const [spy, resolve, reject] = getControllableAsync(); + const { result } = renderHook(() => useAsync(spy, 42)); + + await act(async () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + result.current[1].execute(); + }); + const reject1 = reject.current; + + await act(async () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + result.current[1].execute(); + }); + const resolve2 = resolve.current; + + await act(async () => { + if (reject1) reject1(new Error('some err')); + if (resolve2) resolve2(2); + }); + + expect(result.current[0]).toStrictEqual({ + status: 'success', + error: undefined, + result: 2, + }); + }); + + it('should not change methods between renders', () => { + const spy = jest.fn(async () => {}); + const { rerender, result } = renderHook(() => useAsync(spy)); + + const res1 = result.current; + rerender(); + + expect(res1[1].execute).toBe(result.current[1].execute); + expect(res1[1].reset).toBe(result.current[1].reset); + }); }); diff --git a/src/useAsync/__tests__/ssr.ts b/src/useAsync/__tests__/ssr.ts index bd470d407..e3cc9a705 100644 --- a/src/useAsync/__tests__/ssr.ts +++ b/src/useAsync/__tests__/ssr.ts @@ -2,12 +2,12 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useAsync } from '../..'; describe('useAsync', () => { - it('should be defined', () => { - expect(useAsync).toBeDefined(); - }); + it('should be defined', () => { + expect(useAsync).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useAsync(async () => {})); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useAsync(async () => {})); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useAsync/index.ts b/src/useAsync/index.ts index 2e9f5b003..f982f9f41 100644 --- a/src/useAsync/index.ts +++ b/src/useAsync/index.ts @@ -4,56 +4,56 @@ import { useSyncedRef } from '../useSyncedRef'; export type AsyncStatus = 'loading' | 'success' | 'error' | 'not-executed'; export type AsyncState = - | { - status: 'not-executed'; - error: undefined; - result: Result; - } - | { - status: 'success'; - error: undefined; - result: Result; - } - | { - status: 'error'; - error: Error; - result: Result; - } - | { - status: AsyncStatus; - error: Error | undefined; - result: Result; - }; + | { + status: 'not-executed'; + error: undefined; + result: Result; + } + | { + status: 'success'; + error: undefined; + result: Result; + } + | { + status: 'error'; + error: Error; + result: Result; + } + | { + status: AsyncStatus; + error: Error | undefined; + result: Result; + }; export type UseAsyncActions = { - /** - * Reset state to initial. - */ - reset: () => void; - /** - * Execute the async function manually. - */ - execute: (...args: Args) => Promise; + /** + * Reset state to initial. + */ + reset: () => void; + /** + * Execute the async function manually. + */ + execute: (...args: Args) => Promise; }; export type UseAsyncMeta = { - /** - * Latest promise returned from the async function. - */ - promise: Promise | undefined; - /** - * List of arguments applied to the latest async function invocation. - */ - lastArgs: Args | undefined; + /** + * Latest promise returned from the async function. + */ + promise: Promise | undefined; + /** + * List of arguments applied to the latest async function invocation. + */ + lastArgs: Args | undefined; }; export function useAsync( - asyncFn: (...params: Args) => Promise, - initialValue: Result + asyncFn: (...params: Args) => Promise, + initialValue: Result ): [AsyncState, UseAsyncActions, UseAsyncMeta]; export function useAsync( - asyncFn: (...params: Args) => Promise, - initialValue?: Result + asyncFn: (...params: Args) => Promise, + initialValue?: Result ): [AsyncState, UseAsyncActions, UseAsyncMeta]; /** @@ -64,69 +64,69 @@ export function useAsync( * executed. */ export function useAsync( - asyncFn: (...params: Args) => Promise, - initialValue?: Result + asyncFn: (...params: Args) => Promise, + initialValue?: Result ): [AsyncState, UseAsyncActions, UseAsyncMeta] { - const [state, setState] = useState>({ - status: 'not-executed', - error: undefined, - result: initialValue, - }); - const promiseRef = useRef>(); - const argsRef = useRef(); + const [state, setState] = useState>({ + status: 'not-executed', + error: undefined, + result: initialValue, + }); + const promiseRef = useRef>(); + const argsRef = useRef(); - const methods = useSyncedRef({ - execute(...params: Args) { - argsRef.current = params; - const promise = asyncFn(...params); - promiseRef.current = promise; + const methods = useSyncedRef({ + execute(...params: Args) { + argsRef.current = params; + const promise = asyncFn(...params); + promiseRef.current = promise; - setState((s) => ({ ...s, status: 'loading' })); + setState((s) => ({ ...s, status: 'loading' })); - // eslint-disable-next-line promise/catch-or-return - promise.then( - (result) => { - // We dont want to handle result/error of non-latest function - // this approach helps to avoid race conditions - // eslint-disable-next-line promise/always-return - if (promise === promiseRef.current) { - setState((s) => ({ ...s, status: 'success', error: undefined, result })); - } - }, - (error: Error) => { - // We dont want to handle result/error of non-latest function - // this approach helps to avoid race conditions - if (promise === promiseRef.current) { - setState((s) => ({ ...s, status: 'error', error })); - } - } - ); + // eslint-disable-next-line promise/catch-or-return + promise.then( + (result) => { + // We dont want to handle result/error of non-latest function + // this approach helps to avoid race conditions + // eslint-disable-next-line promise/always-return + if (promise === promiseRef.current) { + setState((s) => ({ ...s, status: 'success', error: undefined, result })); + } + }, + (error: Error) => { + // We dont want to handle result/error of non-latest function + // this approach helps to avoid race conditions + if (promise === promiseRef.current) { + setState((s) => ({ ...s, status: 'error', error })); + } + } + ); - return promise; - }, - reset() { - setState({ - status: 'not-executed', - error: undefined, - result: initialValue, - }); - promiseRef.current = undefined; - argsRef.current = undefined; - }, - }); + return promise; + }, + reset() { + setState({ + status: 'not-executed', + error: undefined, + result: initialValue, + }); + promiseRef.current = undefined; + argsRef.current = undefined; + }, + }); - return [ - state, - useMemo( - () => ({ - reset() { - methods.current.reset(); - }, - execute: (...params: Args) => methods.current.execute(...params), - }), + return [ + state, + useMemo( + () => ({ + reset() { + methods.current.reset(); + }, + execute: (...params: Args) => methods.current.execute(...params), + }), - [] - ), - { promise: promiseRef.current, lastArgs: argsRef.current }, - ]; + [] + ), + { promise: promiseRef.current, lastArgs: argsRef.current }, + ]; } diff --git a/src/useAsyncAbortable/__docs__/example.stories.tsx b/src/useAsyncAbortable/__docs__/example.stories.tsx index 76864a221..81bf08b85 100644 --- a/src/useAsyncAbortable/__docs__/example.stories.tsx +++ b/src/useAsyncAbortable/__docs__/example.stories.tsx @@ -2,57 +2,57 @@ import * as React from 'react'; import { useAsyncAbortable, useMountEffect } from '../..'; export function Example() { - const [state, actions, meta] = useAsyncAbortable( - (signal) => - new Promise((resolve, reject) => { - setTimeout(() => { - if (signal.aborted) { - reject(new Error('Aborted!')); - } else { - resolve('react-hookz is awesome!'); - } - }, 5000); - }), - 'react-hookz is' - ); + const [state, actions, meta] = useAsyncAbortable( + (signal) => + new Promise((resolve, reject) => { + setTimeout(() => { + if (signal.aborted) { + reject(new Error('Aborted!')); + } else { + resolve('react-hookz is awesome!'); + } + }, 5000); + }), + 'react-hookz is' + ); - useMountEffect(actions.execute); + useMountEffect(actions.execute); - return ( -
-
- - Async function will resolve after 5 seconds. If the function is aborted, the promise will - be rejected. - -
-
-
promise status: {state.status}
-
current value: {state.result ?? 'undefined'}
-
error: {state.error ? state.error.message : 'undefined'}
-
-
- {' '} - {' '} - -
-
- ); + return ( +
+
+ + Async function will resolve after 5 seconds. If the function is aborted, the promise will + be rejected. + +
+
+
promise status: {state.status}
+
current value: {state.result ?? 'undefined'}
+
error: {state.error ? state.error.message : 'undefined'}
+
+
+ {' '} + {' '} + +
+
+ ); } diff --git a/src/useAsyncAbortable/__docs__/story.mdx b/src/useAsyncAbortable/__docs__/story.mdx index eee971ab9..14a02b62f 100644 --- a/src/useAsyncAbortable/__docs__/story.mdx +++ b/src/useAsyncAbortable/__docs__/story.mdx @@ -15,19 +15,19 @@ Like `useAsync`, but also provides `AbortSignal` as the first argument to the as #### Example - + ## Reference ```ts export function useAsyncAbortable( - asyncFn: (...params: ArgsWithAbortSignal) => Promise, - initialValue?: Result + asyncFn: (...params: ArgsWithAbortSignal) => Promise, + initialValue?: Result ): [ - AsyncState, - UseAsyncAbortableActions, - UseAsyncAbortableMeta + AsyncState, + UseAsyncAbortableActions, + UseAsyncAbortableMeta ]; ``` diff --git a/src/useAsyncAbortable/__tests__/dom.ts b/src/useAsyncAbortable/__tests__/dom.ts index f2fefe919..38ff5c616 100644 --- a/src/useAsyncAbortable/__tests__/dom.ts +++ b/src/useAsyncAbortable/__tests__/dom.ts @@ -2,143 +2,143 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { useAsyncAbortable } from '../..'; describe('useAsyncAbortable', () => { - function getControllableAsync() { - const resolve: { current: undefined | ((result: Res) => void) } = { current: undefined }; - const reject: { current: undefined | ((err: Error) => void) } = { current: undefined }; - - return [ - jest.fn( - (..._args: Args) => - // eslint-disable-next-line promise/param-names - new Promise((res, rej) => { - resolve.current = res; - reject.current = rej; - }) - ), - resolve, - reject, - ] as const; - } - - it('should be defined', () => { - expect(useAsyncAbortable).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useAsyncAbortable(async (_) => {})); - expect(result.error).toBeUndefined(); - }); - - it('should not change methods between renders', () => { - const spy = jest.fn(async () => {}); - const { rerender, result } = renderHook(() => useAsyncAbortable(spy)); - - const res1 = result.current; - rerender(); - - expect(res1[1].execute).toBe(result.current[1].execute); - expect(res1[1].reset).toBe(result.current[1].reset); - expect(res1[1].abort).toBe(result.current[1].abort); - }); - - it('should pass abort signal as first argument', async () => { - const spy = jest.fn(async (s: AbortSignal, n: number) => n); - const { result } = renderHook(() => useAsyncAbortable(spy)); - - await act(async () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - result.current[1].execute(123); - }); - - expect(spy.mock.calls[0][0]).toBeInstanceOf(AbortSignal); - expect(spy.mock.calls[0][0].aborted).toBe(false); - expect(spy.mock.calls[0][1]).toBe(123); - - expect(result.current[0]).toStrictEqual({ - status: 'success', - error: undefined, - result: 123, - }); - }); - - it('should abort signal in case of actions.abort call', async () => { - const [spy, resolve] = getControllableAsync(); - const { result } = renderHook(() => useAsyncAbortable(spy)); - - await act(async () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - result.current[1].execute(123); - }); - - result.current[1].abort(); - - expect(spy.mock.calls[0][0].aborted).toBe(true); - - await act(async () => { - if (resolve.current) { - resolve.current(123); - } - }); - }); - - it('should also abort signal in case of actions.reset call', async () => { - const [spy, resolve] = getControllableAsync(); - const { result } = renderHook(() => useAsyncAbortable(spy, 321)); - - await act(async () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - result.current[1].execute(123); - }); - - await act(async () => { - result.current[1].reset(); - }); - - expect(spy.mock.calls[0][0].aborted).toBe(true); - - expect(result.current[0]).toStrictEqual({ - status: 'not-executed', - error: undefined, - result: 321, - }); - - await act(async () => { - if (resolve.current) { - resolve.current(123); - } - }); - }); - - it('should abort previous async in case new one executed before first resolution', async () => { - const [spy, resolve] = getControllableAsync(); - const { result } = renderHook(() => useAsyncAbortable(spy, 321)); - - await act(async () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - result.current[1].execute(123); - }); - - const resolve1 = resolve.current; - - await act(async () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - result.current[1].execute(1234); - }); - - expect(spy.mock.calls[0][1]).toBe(123); - expect(spy.mock.calls[0][0].aborted).toBe(true); - - expect(spy.mock.calls[1][1]).toBe(1234); - expect(spy.mock.calls[1][0].aborted).toBe(false); - - await act(async () => { - if (resolve1) { - resolve1(123); - } - - if (resolve.current) { - resolve.current(1234); - } - }); - }); + function getControllableAsync() { + const resolve: { current: undefined | ((result: Res) => void) } = { current: undefined }; + const reject: { current: undefined | ((err: Error) => void) } = { current: undefined }; + + return [ + jest.fn( + (..._args: Args) => + // eslint-disable-next-line promise/param-names + new Promise((res, rej) => { + resolve.current = res; + reject.current = rej; + }) + ), + resolve, + reject, + ] as const; + } + + it('should be defined', () => { + expect(useAsyncAbortable).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useAsyncAbortable(async (_) => {})); + expect(result.error).toBeUndefined(); + }); + + it('should not change methods between renders', () => { + const spy = jest.fn(async () => {}); + const { rerender, result } = renderHook(() => useAsyncAbortable(spy)); + + const res1 = result.current; + rerender(); + + expect(res1[1].execute).toBe(result.current[1].execute); + expect(res1[1].reset).toBe(result.current[1].reset); + expect(res1[1].abort).toBe(result.current[1].abort); + }); + + it('should pass abort signal as first argument', async () => { + const spy = jest.fn(async (s: AbortSignal, n: number) => n); + const { result } = renderHook(() => useAsyncAbortable(spy)); + + await act(async () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + result.current[1].execute(123); + }); + + expect(spy.mock.calls[0][0]).toBeInstanceOf(AbortSignal); + expect(spy.mock.calls[0][0].aborted).toBe(false); + expect(spy.mock.calls[0][1]).toBe(123); + + expect(result.current[0]).toStrictEqual({ + status: 'success', + error: undefined, + result: 123, + }); + }); + + it('should abort signal in case of actions.abort call', async () => { + const [spy, resolve] = getControllableAsync(); + const { result } = renderHook(() => useAsyncAbortable(spy)); + + await act(async () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + result.current[1].execute(123); + }); + + result.current[1].abort(); + + expect(spy.mock.calls[0][0].aborted).toBe(true); + + await act(async () => { + if (resolve.current) { + resolve.current(123); + } + }); + }); + + it('should also abort signal in case of actions.reset call', async () => { + const [spy, resolve] = getControllableAsync(); + const { result } = renderHook(() => useAsyncAbortable(spy, 321)); + + await act(async () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + result.current[1].execute(123); + }); + + await act(async () => { + result.current[1].reset(); + }); + + expect(spy.mock.calls[0][0].aborted).toBe(true); + + expect(result.current[0]).toStrictEqual({ + status: 'not-executed', + error: undefined, + result: 321, + }); + + await act(async () => { + if (resolve.current) { + resolve.current(123); + } + }); + }); + + it('should abort previous async in case new one executed before first resolution', async () => { + const [spy, resolve] = getControllableAsync(); + const { result } = renderHook(() => useAsyncAbortable(spy, 321)); + + await act(async () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + result.current[1].execute(123); + }); + + const resolve1 = resolve.current; + + await act(async () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + result.current[1].execute(1234); + }); + + expect(spy.mock.calls[0][1]).toBe(123); + expect(spy.mock.calls[0][0].aborted).toBe(true); + + expect(spy.mock.calls[1][1]).toBe(1234); + expect(spy.mock.calls[1][0].aborted).toBe(false); + + await act(async () => { + if (resolve1) { + resolve1(123); + } + + if (resolve.current) { + resolve.current(1234); + } + }); + }); }); diff --git a/src/useAsyncAbortable/__tests__/ssr.ts b/src/useAsyncAbortable/__tests__/ssr.ts index bacb6d1df..b42ed4516 100644 --- a/src/useAsyncAbortable/__tests__/ssr.ts +++ b/src/useAsyncAbortable/__tests__/ssr.ts @@ -2,12 +2,12 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useAsyncAbortable } from '../..'; describe('useAsyncAbortable', () => { - it('should be defined', () => { - expect(useAsyncAbortable).toBeDefined(); - }); + it('should be defined', () => { + expect(useAsyncAbortable).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useAsyncAbortable(async (_) => {})); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useAsyncAbortable(async (_) => {})); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useAsyncAbortable/index.ts b/src/useAsyncAbortable/index.ts index b24e4e0d7..5441cd656 100644 --- a/src/useAsyncAbortable/index.ts +++ b/src/useAsyncAbortable/index.ts @@ -2,41 +2,41 @@ import { useMemo, useRef } from 'react'; import { type AsyncState, useAsync, type UseAsyncActions, type UseAsyncMeta } from '../useAsync'; export type UseAsyncAbortableActions = { - /** - * Abort the currently running async function invocation. - */ - abort: () => void; + /** + * Abort the currently running async function invocation. + */ + abort: () => void; - /** - * Abort the currently running async function invocation and reset state to initial. - */ - reset: () => void; + /** + * Abort the currently running async function invocation and reset state to initial. + */ + reset: () => void; } & UseAsyncActions; export type UseAsyncAbortableMeta = { - /** - * Currently used `AbortController`. New one is created on each execution of the async function. - */ - abortController: AbortController | undefined; + /** + * Currently used `AbortController`. New one is created on each execution of the async function. + */ + abortController: AbortController | undefined; } & UseAsyncMeta; export type ArgsWithAbortSignal = [AbortSignal, ...Args]; export function useAsyncAbortable( - asyncFn: (...params: ArgsWithAbortSignal) => Promise, - initialValue: Result + asyncFn: (...params: ArgsWithAbortSignal) => Promise, + initialValue: Result ): [ - AsyncState, - UseAsyncAbortableActions, - UseAsyncAbortableMeta + AsyncState, + UseAsyncAbortableActions, + UseAsyncAbortableMeta ]; export function useAsyncAbortable( - asyncFn: (...params: ArgsWithAbortSignal) => Promise, - initialValue?: Result + asyncFn: (...params: ArgsWithAbortSignal) => Promise, + initialValue?: Result ): [ - AsyncState, - UseAsyncAbortableActions, - UseAsyncAbortableMeta + AsyncState, + UseAsyncAbortableActions, + UseAsyncAbortableMeta ]; /** @@ -47,52 +47,52 @@ export function useAsyncAbortable( * executed. */ export function useAsyncAbortable( - asyncFn: (...params: ArgsWithAbortSignal) => Promise, - initialValue?: Result + asyncFn: (...params: ArgsWithAbortSignal) => Promise, + initialValue?: Result ): [ - AsyncState, - UseAsyncAbortableActions, - UseAsyncAbortableMeta + AsyncState, + UseAsyncAbortableActions, + UseAsyncAbortableMeta ] { - const abortController = useRef(); + const abortController = useRef(); - const fn = async (...args: Args): Promise => { - // Abort previous async - abortController.current?.abort(); + const fn = async (...args: Args): Promise => { + // Abort previous async + abortController.current?.abort(); - // Create new controller for ongoing async call - const ac = new AbortController(); - abortController.current = ac; + // Create new controller for ongoing async call + const ac = new AbortController(); + abortController.current = ac; - // Pass down abort signal and received arguments - return asyncFn(ac.signal, ...args).finally(() => { - // Unset ref uf the call is last - if (abortController.current === ac) { - abortController.current = undefined; - } - }); - }; + // Pass down abort signal and received arguments + return asyncFn(ac.signal, ...args).finally(() => { + // Unset ref uf the call is last + if (abortController.current === ac) { + abortController.current = undefined; + } + }); + }; - const [state, asyncActions, asyncMeta] = useAsync(fn, initialValue); + const [state, asyncActions, asyncMeta] = useAsync(fn, initialValue); - return [ - state, - useMemo(() => { - const actions = { - reset() { - actions.abort(); - asyncActions.reset(); - }, - abort() { - abortController.current?.abort(); - }, - }; + return [ + state, + useMemo(() => { + const actions = { + reset() { + actions.abort(); + asyncActions.reset(); + }, + abort() { + abortController.current?.abort(); + }, + }; - return { - ...asyncActions, - ...actions, - }; - }, []), - { ...asyncMeta, abortController: abortController.current }, - ]; + return { + ...asyncActions, + ...actions, + }; + }, []), + { ...asyncMeta, abortController: abortController.current }, + ]; } diff --git a/src/useClickOutside/__docs__/example.stories.tsx b/src/useClickOutside/__docs__/example.stories.tsx index 59ae4081a..6f4a69fb6 100644 --- a/src/useClickOutside/__docs__/example.stories.tsx +++ b/src/useClickOutside/__docs__/example.stories.tsx @@ -3,54 +3,54 @@ import { useRef } from 'react'; import { useClickOutside, useToggle } from '../..'; export function Example() { - const [toggled, toggle] = useToggle(); + const [toggled, toggle] = useToggle(); - function ToggledComponent() { - const ref = useRef(null); + function ToggledComponent() { + const ref = useRef(null); - useClickOutside(ref, () => { - // eslint-disable-next-line no-alert - window.alert('told ya!'); - toggle(); - }); + useClickOutside(ref, () => { + // eslint-disable-next-line no-alert + window.alert('told ya!'); + toggle(); + }); - return ( -
- DO NOT -
- CLICK OUTSIDE -
- THE RED SQUARE! -
- ); - } + return ( +
+ DO NOT +
+ CLICK OUTSIDE +
+ THE RED SQUARE! +
+ ); + } - return ( -
-
Let's try some reverse psychology =)
-
+ return ( +
+
Let's try some reverse psychology =)
+
- {!toggled && ( - - )} - {toggled && } -
- ); + {!toggled && ( + + )} + {toggled && } +
+ ); } diff --git a/src/useClickOutside/__docs__/story.mdx b/src/useClickOutside/__docs__/story.mdx index 460a3db81..d85da67ab 100644 --- a/src/useClickOutside/__docs__/story.mdx +++ b/src/useClickOutside/__docs__/story.mdx @@ -16,7 +16,7 @@ Triggers a callback when the user clicks outside a targeted element. #### Example - + ## Reference @@ -25,9 +25,9 @@ Triggers a callback when the user clicks outside a targeted element. const DEFAULT_EVENTS = ['mousedown', 'touchstart']; export function useClickOutside( - ref: RefObject | MutableRefObject, - callback: EventListener, - events: string[] = DEFAULT_EVENTS + ref: RefObject | MutableRefObject, + callback: EventListener, + events: string[] = DEFAULT_EVENTS ): void; ``` diff --git a/src/useClickOutside/__tests__/dom.ts b/src/useClickOutside/__tests__/dom.ts index df2512733..c787345df 100644 --- a/src/useClickOutside/__tests__/dom.ts +++ b/src/useClickOutside/__tests__/dom.ts @@ -3,109 +3,109 @@ import { type MutableRefObject } from 'react'; import { useClickOutside } from '../..'; describe('useClickOutside', () => { - it('should be defined', () => { - expect(useClickOutside).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => { - useClickOutside({ current: null }, () => {}); - }); - expect(result.error).toBeUndefined(); - }); - - it('should bind document listener on mount and unbind on unmount', () => { - const div = document.createElement('div'); - const addSpy = jest.spyOn(document, 'addEventListener'); - const removeSpy = jest.spyOn(document, 'removeEventListener'); - - const { rerender, unmount } = renderHook(() => { - useClickOutside({ current: div }, () => {}); - }); - - expect(addSpy).toHaveBeenCalledTimes(2); - expect(removeSpy).toHaveBeenCalledTimes(0); - - rerender(); - expect(addSpy).toHaveBeenCalledTimes(2); - expect(removeSpy).toHaveBeenCalledTimes(0); - - unmount(); - expect(addSpy).toHaveBeenCalledTimes(2); - expect(removeSpy).toHaveBeenCalledTimes(2); - - addSpy.mockRestore(); - removeSpy.mockRestore(); - }); - - it('should bind any events passed as 3rd parameter', () => { - const div = document.createElement('div'); - const addSpy = jest.spyOn(document, 'addEventListener'); - const removeSpy = jest.spyOn(document, 'removeEventListener'); - - const { unmount } = renderHook(() => { - useClickOutside({ current: div }, () => {}, ['click']); - }); - - expect(addSpy).toHaveBeenCalledTimes(1); - expect(removeSpy).toHaveBeenCalledTimes(0); - - unmount(); - expect(addSpy).toHaveBeenCalledTimes(1); - expect(removeSpy).toHaveBeenCalledTimes(1); - - addSpy.mockRestore(); - removeSpy.mockRestore(); - }); - - it('should invoke callback if event target is not a child of target', () => { - const div = document.createElement('div'); - const div2 = document.createElement('div2'); - const spy = jest.fn(); - - renderHook(() => { - useClickOutside({ current: div }, spy); - }); - - document.body.append(div, div2); - - div2.dispatchEvent(new Event('mousedown', { bubbles: true })); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it('should not execute callback if event target is a child of target', () => { - const div = document.createElement('div'); - const div2 = document.createElement('div2'); - const spy = jest.fn(); - - renderHook(() => { - useClickOutside({ current: div }, spy); - }); - - document.body.append(div); - div.append(div2); - - div2.dispatchEvent(new Event('mousedown', { bubbles: true })); - expect(spy).not.toHaveBeenCalled(); - }); - - it('should not execute callback if target is unmounted', () => { - const div = document.createElement('div'); - const div2 = document.createElement('div2'); - const spy = jest.fn(); - const ref: MutableRefObject = { current: div }; - - const { rerender } = renderHook(() => { - useClickOutside(ref, spy); - }); - - document.body.append(div); - div.append(div2); - - ref.current = null; - rerender(); - - div2.dispatchEvent(new Event('mousedown', { bubbles: true })); - expect(spy).not.toHaveBeenCalled(); - }); + it('should be defined', () => { + expect(useClickOutside).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => { + useClickOutside({ current: null }, () => {}); + }); + expect(result.error).toBeUndefined(); + }); + + it('should bind document listener on mount and unbind on unmount', () => { + const div = document.createElement('div'); + const addSpy = jest.spyOn(document, 'addEventListener'); + const removeSpy = jest.spyOn(document, 'removeEventListener'); + + const { rerender, unmount } = renderHook(() => { + useClickOutside({ current: div }, () => {}); + }); + + expect(addSpy).toHaveBeenCalledTimes(2); + expect(removeSpy).toHaveBeenCalledTimes(0); + + rerender(); + expect(addSpy).toHaveBeenCalledTimes(2); + expect(removeSpy).toHaveBeenCalledTimes(0); + + unmount(); + expect(addSpy).toHaveBeenCalledTimes(2); + expect(removeSpy).toHaveBeenCalledTimes(2); + + addSpy.mockRestore(); + removeSpy.mockRestore(); + }); + + it('should bind any events passed as 3rd parameter', () => { + const div = document.createElement('div'); + const addSpy = jest.spyOn(document, 'addEventListener'); + const removeSpy = jest.spyOn(document, 'removeEventListener'); + + const { unmount } = renderHook(() => { + useClickOutside({ current: div }, () => {}, ['click']); + }); + + expect(addSpy).toHaveBeenCalledTimes(1); + expect(removeSpy).toHaveBeenCalledTimes(0); + + unmount(); + expect(addSpy).toHaveBeenCalledTimes(1); + expect(removeSpy).toHaveBeenCalledTimes(1); + + addSpy.mockRestore(); + removeSpy.mockRestore(); + }); + + it('should invoke callback if event target is not a child of target', () => { + const div = document.createElement('div'); + const div2 = document.createElement('div2'); + const spy = jest.fn(); + + renderHook(() => { + useClickOutside({ current: div }, spy); + }); + + document.body.append(div, div2); + + div2.dispatchEvent(new Event('mousedown', { bubbles: true })); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should not execute callback if event target is a child of target', () => { + const div = document.createElement('div'); + const div2 = document.createElement('div2'); + const spy = jest.fn(); + + renderHook(() => { + useClickOutside({ current: div }, spy); + }); + + document.body.append(div); + div.append(div2); + + div2.dispatchEvent(new Event('mousedown', { bubbles: true })); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should not execute callback if target is unmounted', () => { + const div = document.createElement('div'); + const div2 = document.createElement('div2'); + const spy = jest.fn(); + const ref: MutableRefObject = { current: div }; + + const { rerender } = renderHook(() => { + useClickOutside(ref, spy); + }); + + document.body.append(div); + div.append(div2); + + ref.current = null; + rerender(); + + div2.dispatchEvent(new Event('mousedown', { bubbles: true })); + expect(spy).not.toHaveBeenCalled(); + }); }); diff --git a/src/useClickOutside/__tests__/ssr.ts b/src/useClickOutside/__tests__/ssr.ts index bedb187f5..55d0fdff2 100644 --- a/src/useClickOutside/__tests__/ssr.ts +++ b/src/useClickOutside/__tests__/ssr.ts @@ -2,14 +2,14 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useClickOutside } from '../..'; describe('useClickOutside', () => { - it('should be defined', () => { - expect(useClickOutside).toBeDefined(); - }); + it('should be defined', () => { + expect(useClickOutside).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useClickOutside({ current: null }, () => {}); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useClickOutside({ current: null }, () => {}); + }); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useClickOutside/index.ts b/src/useClickOutside/index.ts index 521133646..ddeccc642 100644 --- a/src/useClickOutside/index.ts +++ b/src/useClickOutside/index.ts @@ -13,36 +13,36 @@ const DEFAULT_EVENTS = ['mousedown', 'touchstart']; * 'mousedown', 'touchstart' */ export function useClickOutside( - ref: RefObject | MutableRefObject, - callback: EventListener, - events: string[] = DEFAULT_EVENTS + ref: RefObject | MutableRefObject, + callback: EventListener, + events: string[] = DEFAULT_EVENTS ): void { - const cbRef = useSyncedRef(callback); - const refRef = useSyncedRef(ref); + const cbRef = useSyncedRef(callback); + const refRef = useSyncedRef(ref); - useEffect(() => { - function handler(this: HTMLElement, event: Event) { - if (!refRef.current.current) return; + useEffect(() => { + function handler(this: HTMLElement, event: Event) { + if (!refRef.current.current) return; - const { target: evtTarget } = event; - const cb = cbRef.current; + const { target: evtTarget } = event; + const cb = cbRef.current; - if ( - !evtTarget || - (Boolean(evtTarget) && !refRef.current.current.contains(evtTarget as Node)) - ) { - cb.call(this, event); - } - } + if ( + !evtTarget || + (Boolean(evtTarget) && !refRef.current.current.contains(evtTarget as Node)) + ) { + cb.call(this, event); + } + } - events.forEach((name) => { - on(document, name, handler, { passive: true }); - }); + events.forEach((name) => { + on(document, name, handler, { passive: true }); + }); - return () => { - events.forEach((name) => { - off(document, name, handler, { passive: true }); - }); - }; - }, [...events]); + return () => { + events.forEach((name) => { + off(document, name, handler, { passive: true }); + }); + }; + }, [...events]); } diff --git a/src/useConditionalEffect/__docs__/example.stories.tsx b/src/useConditionalEffect/__docs__/example.stories.tsx index 2ef3b4419..f447d6061 100644 --- a/src/useConditionalEffect/__docs__/example.stories.tsx +++ b/src/useConditionalEffect/__docs__/example.stories.tsx @@ -3,37 +3,37 @@ import { useState } from 'react'; import { useConditionalEffect, useUpdateEffect } from '../..'; export function Example() { - const [state1, setState1] = useState(2); - const [state2, setState2] = useState(2); + const [state1, setState1] = useState(2); + const [state2, setState2] = useState(2); - useConditionalEffect( - () => { - // eslint-disable-next-line no-alert - alert('VALUES OF THE COUNTERS ARE EVEN'); - }, - [state1, state2], - [state1, state2], - (conditions) => conditions.every((i) => i && i % 2 === 0), - useUpdateEffect - ); + useConditionalEffect( + () => { + // eslint-disable-next-line no-alert + alert('VALUES OF THE COUNTERS ARE EVEN'); + }, + [state1, state2], + [state1, state2], + (conditions) => conditions.every((i) => i && i % 2 === 0), + useUpdateEffect + ); - return ( -
-
An alert will be displayed when both counters have even values.
- {' '} - -
- ); + return ( +
+
An alert will be displayed when both counters have even values.
+ {' '} + +
+ ); } diff --git a/src/useConditionalEffect/__docs__/story.mdx b/src/useConditionalEffect/__docs__/story.mdx index 0d460705c..2ffae1080 100644 --- a/src/useConditionalEffect/__docs__/story.mdx +++ b/src/useConditionalEffect/__docs__/story.mdx @@ -16,25 +16,25 @@ other effect hooks this way, you can implement almost any effect logic. #### Example - + ## Reference ```ts export function useConditionalEffect< - Cond extends ConditionsList, - Callback extends EffectCallback = EffectCallback, - Deps extends DependencyList | undefined = DependencyList | undefined, - HookRestArgs extends any[] = any[], - R extends HookRestArgs = HookRestArgs + Cond extends ConditionsList, + Callback extends EffectCallback = EffectCallback, + Deps extends DependencyList | undefined = DependencyList | undefined, + HookRestArgs extends any[] = any[], + R extends HookRestArgs = HookRestArgs >( - callback: Callback, - deps: Deps, - conditions: Cond, - predicate: ConditionsPredicate = truthyAndArrayPredicate, - effectHook: EffectHook = useEffect, - ...effectHookRestArgs: R + callback: Callback, + deps: Deps, + conditions: Cond, + predicate: ConditionsPredicate = truthyAndArrayPredicate, + effectHook: EffectHook = useEffect, + ...effectHookRestArgs: R ): void; ``` diff --git a/src/useConditionalEffect/__tests__/dom.ts b/src/useConditionalEffect/__tests__/dom.ts index 211662250..f3bf6dc97 100644 --- a/src/useConditionalEffect/__tests__/dom.ts +++ b/src/useConditionalEffect/__tests__/dom.ts @@ -1,134 +1,134 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { type DependencyList, type EffectCallback } from 'react'; import { - truthyAndArrayPredicate, - truthyOrArrayPredicate, - useConditionalEffect, - useUpdateEffect, + truthyAndArrayPredicate, + truthyOrArrayPredicate, + useConditionalEffect, + useUpdateEffect, } from '../..'; describe('useConditionalEffect', () => { - it('should be defined', () => { - expect(useConditionalEffect).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => { - useConditionalEffect(() => {}, undefined, []); - }); - expect(result.error).toBeUndefined(); - }); - - it('by default should invoke effect only in case all conditions are truthy', () => { - const spy = jest.fn(); - const { rerender } = renderHook( - ({ cond }) => { - useConditionalEffect(spy, undefined, cond); - }, - { - initialProps: { cond: [1] as unknown[] }, - } - ); - expect(spy).toHaveBeenCalledTimes(1); - - rerender({ cond: [0, 1, 1] }); - expect(spy).toHaveBeenCalledTimes(1); - - rerender({ cond: [1, {}, null] }); - expect(spy).toHaveBeenCalledTimes(1); - - rerender({ cond: [true, {}, [], 25] }); - expect(spy).toHaveBeenCalledTimes(2); - }); - - it('should not be called on mount if conditions are falsy', () => { - const spy = jest.fn(); - renderHook( - ({ cond }) => { - useConditionalEffect(spy, undefined, cond); - }, - { - initialProps: { cond: [null] as unknown[] }, - } - ); - expect(spy).toHaveBeenCalledTimes(0); - }); - - it('should invoke callback only if deps are changed and conditions match predicate', () => { - const spy = jest.fn(); - const { rerender } = renderHook( - ({ cond, deps }) => { - useConditionalEffect(spy, deps, cond); - }, - { - initialProps: { cond: [false] as unknown[], deps: [1] as any[] }, - } - ); - expect(spy).toHaveBeenCalledTimes(0); - - rerender({ cond: [false], deps: [2] }); - expect(spy).toHaveBeenCalledTimes(0); - - rerender({ cond: [true], deps: [2] }); - expect(spy).toHaveBeenCalledTimes(0); - - rerender({ cond: [true], deps: [3] }); - expect(spy).toHaveBeenCalledTimes(1); - - rerender({ cond: [true], deps: [3] }); - expect(spy).toHaveBeenCalledTimes(1); - - rerender({ cond: [true], deps: [4] }); - expect(spy).toHaveBeenCalledTimes(2); - - rerender({ cond: [false], deps: [5] }); - expect(spy).toHaveBeenCalledTimes(2); - }); - - it('should apply custom predicate', () => { - const spy = jest.fn(); - const predicateSpy = jest.fn((conditions) => truthyOrArrayPredicate(conditions)); - const { rerender } = renderHook( - ({ cond }) => { - useConditionalEffect(spy, undefined, cond, predicateSpy); - }, - { - initialProps: { cond: [null] as unknown[] }, - } - ); - expect(predicateSpy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledTimes(0); - - rerender({ cond: [true, {}, [], 25] }); - expect(predicateSpy).toHaveBeenCalledTimes(2); - expect(spy).toHaveBeenCalledTimes(1); - - rerender({ cond: [true, false, 0, null] }); - expect(predicateSpy).toHaveBeenCalledTimes(3); - expect(spy).toHaveBeenCalledTimes(2); - - rerender({ cond: [undefined, false, 0, null] }); - expect(predicateSpy).toHaveBeenCalledTimes(4); - expect(spy).toHaveBeenCalledTimes(2); - }); - - it('should accept custom hooks and pass extra args to it', () => { - const callbackSpy = jest.fn(); - const effectSpy = jest.fn( - (cb: EffectCallback, deps: DependencyList | undefined, _num: number) => { - useUpdateEffect(cb, deps); - } - ); - const { rerender } = renderHook(() => { - useConditionalEffect(callbackSpy, undefined, [true], truthyAndArrayPredicate, effectSpy, 123); - }); - - expect(callbackSpy).not.toHaveBeenCalled(); - expect(effectSpy).toHaveBeenCalledTimes(1); - expect(effectSpy.mock.calls[0][2]).toBe(123); - - rerender(); - - expect(callbackSpy).toHaveBeenCalledTimes(1); - }); + it('should be defined', () => { + expect(useConditionalEffect).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => { + useConditionalEffect(() => {}, undefined, []); + }); + expect(result.error).toBeUndefined(); + }); + + it('by default should invoke effect only in case all conditions are truthy', () => { + const spy = jest.fn(); + const { rerender } = renderHook( + ({ cond }) => { + useConditionalEffect(spy, undefined, cond); + }, + { + initialProps: { cond: [1] as unknown[] }, + } + ); + expect(spy).toHaveBeenCalledTimes(1); + + rerender({ cond: [0, 1, 1] }); + expect(spy).toHaveBeenCalledTimes(1); + + rerender({ cond: [1, {}, null] }); + expect(spy).toHaveBeenCalledTimes(1); + + rerender({ cond: [true, {}, [], 25] }); + expect(spy).toHaveBeenCalledTimes(2); + }); + + it('should not be called on mount if conditions are falsy', () => { + const spy = jest.fn(); + renderHook( + ({ cond }) => { + useConditionalEffect(spy, undefined, cond); + }, + { + initialProps: { cond: [null] as unknown[] }, + } + ); + expect(spy).toHaveBeenCalledTimes(0); + }); + + it('should invoke callback only if deps are changed and conditions match predicate', () => { + const spy = jest.fn(); + const { rerender } = renderHook( + ({ cond, deps }) => { + useConditionalEffect(spy, deps, cond); + }, + { + initialProps: { cond: [false] as unknown[], deps: [1] as any[] }, + } + ); + expect(spy).toHaveBeenCalledTimes(0); + + rerender({ cond: [false], deps: [2] }); + expect(spy).toHaveBeenCalledTimes(0); + + rerender({ cond: [true], deps: [2] }); + expect(spy).toHaveBeenCalledTimes(0); + + rerender({ cond: [true], deps: [3] }); + expect(spy).toHaveBeenCalledTimes(1); + + rerender({ cond: [true], deps: [3] }); + expect(spy).toHaveBeenCalledTimes(1); + + rerender({ cond: [true], deps: [4] }); + expect(spy).toHaveBeenCalledTimes(2); + + rerender({ cond: [false], deps: [5] }); + expect(spy).toHaveBeenCalledTimes(2); + }); + + it('should apply custom predicate', () => { + const spy = jest.fn(); + const predicateSpy = jest.fn((conditions) => truthyOrArrayPredicate(conditions)); + const { rerender } = renderHook( + ({ cond }) => { + useConditionalEffect(spy, undefined, cond, predicateSpy); + }, + { + initialProps: { cond: [null] as unknown[] }, + } + ); + expect(predicateSpy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledTimes(0); + + rerender({ cond: [true, {}, [], 25] }); + expect(predicateSpy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledTimes(1); + + rerender({ cond: [true, false, 0, null] }); + expect(predicateSpy).toHaveBeenCalledTimes(3); + expect(spy).toHaveBeenCalledTimes(2); + + rerender({ cond: [undefined, false, 0, null] }); + expect(predicateSpy).toHaveBeenCalledTimes(4); + expect(spy).toHaveBeenCalledTimes(2); + }); + + it('should accept custom hooks and pass extra args to it', () => { + const callbackSpy = jest.fn(); + const effectSpy = jest.fn( + (cb: EffectCallback, deps: DependencyList | undefined, _num: number) => { + useUpdateEffect(cb, deps); + } + ); + const { rerender } = renderHook(() => { + useConditionalEffect(callbackSpy, undefined, [true], truthyAndArrayPredicate, effectSpy, 123); + }); + + expect(callbackSpy).not.toHaveBeenCalled(); + expect(effectSpy).toHaveBeenCalledTimes(1); + expect(effectSpy.mock.calls[0][2]).toBe(123); + + rerender(); + + expect(callbackSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/useConditionalEffect/__tests__/ssr.ts b/src/useConditionalEffect/__tests__/ssr.ts index 7a4ae07d2..b1c102532 100644 --- a/src/useConditionalEffect/__tests__/ssr.ts +++ b/src/useConditionalEffect/__tests__/ssr.ts @@ -2,24 +2,24 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useConditionalEffect } from '../..'; describe('useConditionalEffect', () => { - it('should be defined', () => { - expect(useConditionalEffect).toBeDefined(); - }); + it('should be defined', () => { + expect(useConditionalEffect).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useConditionalEffect(() => {}, undefined, []); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useConditionalEffect(() => {}, undefined, []); + }); + expect(result.error).toBeUndefined(); + }); - it('should not invoke nor effect nor predicate', () => { - const spy = jest.fn(); - const predicateSpy = jest.fn((arr: unknown[]) => arr.some(Boolean)); - renderHook(() => { - useConditionalEffect(spy, undefined, [true], predicateSpy); - }); - expect(predicateSpy).toHaveBeenCalledTimes(0); - expect(spy).toHaveBeenCalledTimes(0); - }); + it('should not invoke nor effect nor predicate', () => { + const spy = jest.fn(); + const predicateSpy = jest.fn((arr: unknown[]) => arr.some(Boolean)); + renderHook(() => { + useConditionalEffect(spy, undefined, [true], predicateSpy); + }); + expect(predicateSpy).toHaveBeenCalledTimes(0); + expect(spy).toHaveBeenCalledTimes(0); + }); }); diff --git a/src/useConditionalEffect/index.ts b/src/useConditionalEffect/index.ts index dcecf0bcb..deb7599f1 100644 --- a/src/useConditionalEffect/index.ts +++ b/src/useConditionalEffect/index.ts @@ -21,27 +21,27 @@ import type { ConditionsList, ConditionsPredicate } from '../types'; */ // eslint-disable-next-line max-params export function useConditionalEffect< - Cond extends ConditionsList, - Callback extends EffectCallback = EffectCallback, - Deps extends DependencyList | undefined = DependencyList | undefined, - HookRestArgs extends any[] = any[], - R extends HookRestArgs = HookRestArgs + Cond extends ConditionsList, + Callback extends EffectCallback = EffectCallback, + Deps extends DependencyList | undefined = DependencyList | undefined, + HookRestArgs extends any[] = any[], + R extends HookRestArgs = HookRestArgs >( - callback: Callback, - deps: Deps, - conditions: Cond, - predicate: ConditionsPredicate = truthyAndArrayPredicate, - effectHook: EffectHook = useEffect, - ...effectHookRestArgs: R + callback: Callback, + deps: Deps, + conditions: Cond, + predicate: ConditionsPredicate = truthyAndArrayPredicate, + effectHook: EffectHook = useEffect, + ...effectHookRestArgs: R ): void { - effectHook( - (() => { - if (predicate(conditions)) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return callback(); - } - }) as Callback, - deps, - ...effectHookRestArgs - ); + effectHook( + (() => { + if (predicate(conditions)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return callback(); + } + }) as Callback, + deps, + ...effectHookRestArgs + ); } diff --git a/src/useControlledRerenderState/__docs__/example.stories.tsx b/src/useControlledRerenderState/__docs__/example.stories.tsx index d77af4645..23dd4877c 100644 --- a/src/useControlledRerenderState/__docs__/example.stories.tsx +++ b/src/useControlledRerenderState/__docs__/example.stories.tsx @@ -2,28 +2,28 @@ import * as React from 'react'; import { useControlledRerenderState, useToggle } from '../..'; export function Example() { - const [state, setState] = useControlledRerenderState(0); - const [doRerender, toggleDoRerender] = useToggle(true); + const [state, setState] = useControlledRerenderState(0); + const [doRerender, toggleDoRerender] = useToggle(true); - return ( -
-
State: {state}
-

- {' '} - -

-
- ); + return ( +
+
State: {state}
+

+ {' '} + +

+
+ ); } diff --git a/src/useControlledRerenderState/__docs__/story.mdx b/src/useControlledRerenderState/__docs__/story.mdx index 8b232e187..faf544335 100644 --- a/src/useControlledRerenderState/__docs__/story.mdx +++ b/src/useControlledRerenderState/__docs__/story.mdx @@ -13,7 +13,7 @@ Like `React.useState`, but its state setter accepts extra argument, that allows #### Example - + ## Reference @@ -22,11 +22,11 @@ Like `React.useState`, but its state setter accepts extra argument, that allows export type ControlledRerenderDispatch = (value: A, rerender?: boolean) => void; export function useControlledRerenderState( - initialState: S | (() => S) + initialState: S | (() => S) ): [S, ControlledRerenderDispatch>]; export function useControlledRerenderState(): [ - S | undefined, - ControlledRerenderDispatch> + S | undefined, + ControlledRerenderDispatch> ]; ``` diff --git a/src/useControlledRerenderState/__tests__/dom.ts b/src/useControlledRerenderState/__tests__/dom.ts index c127e2aac..7a88772c1 100644 --- a/src/useControlledRerenderState/__tests__/dom.ts +++ b/src/useControlledRerenderState/__tests__/dom.ts @@ -2,44 +2,44 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { useControlledRerenderState } from '../..'; describe('useControlledRerenderState', () => { - it('should be defined', () => { - expect(useControlledRerenderState).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useControlledRerenderState()); - expect(result.error).toBeUndefined(); - }); - - it('should behave as `useState` by default', () => { - const { result } = renderHook(() => useControlledRerenderState(() => 0)); - - expect(result.current[0]).toBe(0); - - act(() => { - result.current[1](1); - }); - expect(result.current[0]).toBe(1); - - act(() => { - result.current[1]((i) => i + 3); - }); - expect(result.current[0]).toBe(4); - }); - - it('should not re-render in case setter extra-argument set to false', () => { - const { result } = renderHook(() => useControlledRerenderState(() => 0)); - - expect(result.current[0]).toBe(0); - - act(() => { - result.current[1](1, false); - }); - expect(result.current[0]).toBe(0); - - act(() => { - result.current[1]((i) => i + 3); - }); - expect(result.current[0]).toBe(4); - }); + it('should be defined', () => { + expect(useControlledRerenderState).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useControlledRerenderState()); + expect(result.error).toBeUndefined(); + }); + + it('should behave as `useState` by default', () => { + const { result } = renderHook(() => useControlledRerenderState(() => 0)); + + expect(result.current[0]).toBe(0); + + act(() => { + result.current[1](1); + }); + expect(result.current[0]).toBe(1); + + act(() => { + result.current[1]((i) => i + 3); + }); + expect(result.current[0]).toBe(4); + }); + + it('should not re-render in case setter extra-argument set to false', () => { + const { result } = renderHook(() => useControlledRerenderState(() => 0)); + + expect(result.current[0]).toBe(0); + + act(() => { + result.current[1](1, false); + }); + expect(result.current[0]).toBe(0); + + act(() => { + result.current[1]((i) => i + 3); + }); + expect(result.current[0]).toBe(4); + }); }); diff --git a/src/useControlledRerenderState/__tests__/ssr.ts b/src/useControlledRerenderState/__tests__/ssr.ts index 20882003a..9b4b88b1d 100644 --- a/src/useControlledRerenderState/__tests__/ssr.ts +++ b/src/useControlledRerenderState/__tests__/ssr.ts @@ -2,12 +2,12 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useControlledRerenderState } from '../..'; describe('useControlledRerenderState', () => { - it('should be defined', () => { - expect(useControlledRerenderState).toBeDefined(); - }); + it('should be defined', () => { + expect(useControlledRerenderState).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useControlledRerenderState()); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useControlledRerenderState()); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useControlledRerenderState/index.ts b/src/useControlledRerenderState/index.ts index 1510db165..7fa790bce 100644 --- a/src/useControlledRerenderState/index.ts +++ b/src/useControlledRerenderState/index.ts @@ -6,11 +6,11 @@ import { resolveHookState } from '../util/resolveHookState'; export type ControlledRerenderDispatch = (value: A, rerender?: boolean) => void; export function useControlledRerenderState( - initialState: S | (() => S) + initialState: S | (() => S) ): [S, ControlledRerenderDispatch>]; export function useControlledRerenderState(): [ - S | undefined, - ControlledRerenderDispatch> + S | undefined, + ControlledRerenderDispatch> ]; /** @@ -18,25 +18,25 @@ export function useControlledRerenderState(): [ * rerender. */ export function useControlledRerenderState( - initialState?: S | (() => S) + initialState?: S | (() => S) ): [S | undefined, ControlledRerenderDispatch>] { - const state = useRef( - useFirstMountState() ? resolveHookState(initialState) : undefined - ); - const rr = useRerender(); + const state = useRef( + useFirstMountState() ? resolveHookState(initialState) : undefined + ); + const rr = useRerender(); - return [ - state.current, - useCallback((value, rerender) => { - const newState = resolveHookState(value, state.current); + return [ + state.current, + useCallback((value, rerender) => { + const newState = resolveHookState(value, state.current); - if (newState !== state.current) { - state.current = newState; + if (newState !== state.current) { + state.current = newState; - if (rerender === undefined || rerender) { - rr(); - } - } - }, []), - ]; + if (rerender === undefined || rerender) { + rr(); + } + } + }, []), + ]; } diff --git a/src/useCookieValue/__docs__/example.stories.tsx b/src/useCookieValue/__docs__/example.stories.tsx index 1fbc0c0c7..02ba8b71a 100644 --- a/src/useCookieValue/__docs__/example.stories.tsx +++ b/src/useCookieValue/__docs__/example.stories.tsx @@ -2,32 +2,32 @@ import * as React from 'react'; import { useCookieValue } from '..'; export function Example() { - const [cookie, set, remove] = useCookieValue('react-hookz', { expires: 3600 }); + const [cookie, set, remove] = useCookieValue('react-hookz', { expires: 3600 }); - return ( -
-
- Cookie name: react-hookz -
-
- Cookie value: {cookie} -
-
- { - set(ev.target.value); - }} - /> -
-
-
- -
-
- ); + return ( +
+
+ Cookie name: react-hookz +
+
+ Cookie value: {cookie} +
+
+ { + set(ev.target.value); + }} + /> +
+
+
+ +
+
+ ); } diff --git a/src/useCookieValue/__docs__/story.mdx b/src/useCookieValue/__docs__/story.mdx index e969d2f65..10289c412 100644 --- a/src/useCookieValue/__docs__/story.mdx +++ b/src/useCookieValue/__docs__/story.mdx @@ -30,23 +30,23 @@ Manages a single cookie. #### Example - - - + + + ## Reference ```ts export type UseCookieOptions = Cookies.CookieAttributes & { - initializeWithValue?: boolean; + initializeWithValue?: boolean; }; export type UseCookieReturn = [ - value: undefined | null | string, - set: (value: string) => void, - remove: () => void, - fetch: () => void + value: undefined | null | string, + set: (value: string) => void, + remove: () => void, + fetch: () => void ]; export function useCookieValue(key: string, options: UseCookieOptions = {}): UseCookieReturn; diff --git a/src/useCookieValue/__tests__/dom.ts b/src/useCookieValue/__tests__/dom.ts index bdff241e6..2c08802f3 100644 --- a/src/useCookieValue/__tests__/dom.ts +++ b/src/useCookieValue/__tests__/dom.ts @@ -4,136 +4,136 @@ import { type UseCookieValueReturn, useCookieValue } from '..'; import SpyInstance = jest.SpyInstance; describe('useCookieValue', () => { - type CookiesGet = typeof Cookies.get; - type CookiesSet = typeof Cookies.set; - type CookiesRemove = typeof Cookies.remove; - - let getSpy: SpyInstance, Parameters>; - let setSpy: SpyInstance, Parameters>; - let removeSpy: SpyInstance, Parameters>; - - beforeAll(() => { - getSpy = jest.spyOn(Cookies, 'get'); - setSpy = jest.spyOn(Cookies, 'set'); - removeSpy = jest.spyOn(Cookies, 'remove'); - }); - - afterAll(() => { - getSpy.mockRestore(); - setSpy.mockRestore(); - removeSpy.mockRestore(); - }); - - beforeEach(() => { - getSpy.mockClear(); - setSpy.mockClear(); - removeSpy.mockClear(); - }); - - it('should be defined', () => { - expect(useCookieValue).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useCookieValue('react-hookz')); - expect(result.error).toBeUndefined(); - }); - - it('should return cookie value on first render', () => { - Cookies.set('react-hookz', 'awesome'); - - const { result } = renderHook(() => useCookieValue('react-hookz')); - expect((result.all[0] as UseCookieValueReturn)[0]).toBe('awesome'); - - Cookies.remove('react-hookz'); - }); - - it('should return undefined on first render if `initializeWithValue` set to false', () => { - const { result } = renderHook(() => - useCookieValue('react-hookz', { initializeWithValue: false }) - ); - expect((result.all[0] as UseCookieValueReturn)[0]).toBeUndefined(); - }); - - it('should return null if cookie not exists', () => { - const { result } = renderHook(() => useCookieValue('react-hookz')); - expect(result.current[0]).toBe(null); - expect(getSpy).toHaveBeenCalledWith('react-hookz'); - }); - - it('should set the cookie value on call to `set`', () => { - const { result } = renderHook(() => useCookieValue('react-hookz')); - - expect(result.current[0]).toBe(null); - act(() => { - result.current[1]('awesome'); - }); - expect(result.current[0]).toBe('awesome'); - expect(setSpy).toHaveBeenCalledWith('react-hookz', 'awesome', {}); - Cookies.remove('react-hookz'); - }); - - it('should remove cookie value on call to `remove`', () => { - const { result } = renderHook(() => useCookieValue('react-hookz')); - - expect(result.current[0]).toBe(null); - act(() => { - result.current[1]('awesome'); - }); - expect(result.current[0]).toBe('awesome'); - - act(() => { - result.current[2](); - }); - expect(result.current[0]).toBe(null); - expect(removeSpy).toHaveBeenCalledWith('react-hookz', {}); - Cookies.remove('react-hookz'); - }); - - it('should re-fetch cookie value on call to `fetch`', () => { - const { result } = renderHook(() => useCookieValue('react-hookz')); - - Cookies.set('react-hookz', 'rulez'); - expect(result.current[0]).toBe(null); - act(() => { - result.current[3](); - }); - expect(result.current[0]).toBe('rulez'); - - Cookies.remove('react-hookz'); - }); - - it('should be synchronized between several hooks with the same key', () => { - const { result: res1 } = renderHook(() => useCookieValue('react-hookz')); - const { result: res2 } = renderHook(() => useCookieValue('react-hookz')); - - expect(res1.current[0]).toBe(null); - expect(res2.current[0]).toBe(null); - - act(() => { - res1.current[1]('awesome'); - }); - - expect(res1.current[0]).toBe('awesome'); - expect(res2.current[0]).toBe('awesome'); - - act(() => { - res2.current[2](); - }); - - expect(res1.current[0]).toBe(null); - expect(res2.current[0]).toBe(null); - }); - - it('should return stable methods', () => { - const { result, rerender } = renderHook(() => useCookieValue('react-hookz')); - - const res1 = result.current; - - rerender(); - - expect(res1[1]).toBe(result.current[1]); - expect(res1[2]).toBe(result.current[2]); - expect(res1[3]).toBe(result.current[3]); - }); + type CookiesGet = typeof Cookies.get; + type CookiesSet = typeof Cookies.set; + type CookiesRemove = typeof Cookies.remove; + + let getSpy: SpyInstance, Parameters>; + let setSpy: SpyInstance, Parameters>; + let removeSpy: SpyInstance, Parameters>; + + beforeAll(() => { + getSpy = jest.spyOn(Cookies, 'get'); + setSpy = jest.spyOn(Cookies, 'set'); + removeSpy = jest.spyOn(Cookies, 'remove'); + }); + + afterAll(() => { + getSpy.mockRestore(); + setSpy.mockRestore(); + removeSpy.mockRestore(); + }); + + beforeEach(() => { + getSpy.mockClear(); + setSpy.mockClear(); + removeSpy.mockClear(); + }); + + it('should be defined', () => { + expect(useCookieValue).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useCookieValue('react-hookz')); + expect(result.error).toBeUndefined(); + }); + + it('should return cookie value on first render', () => { + Cookies.set('react-hookz', 'awesome'); + + const { result } = renderHook(() => useCookieValue('react-hookz')); + expect((result.all[0] as UseCookieValueReturn)[0]).toBe('awesome'); + + Cookies.remove('react-hookz'); + }); + + it('should return undefined on first render if `initializeWithValue` set to false', () => { + const { result } = renderHook(() => + useCookieValue('react-hookz', { initializeWithValue: false }) + ); + expect((result.all[0] as UseCookieValueReturn)[0]).toBeUndefined(); + }); + + it('should return null if cookie not exists', () => { + const { result } = renderHook(() => useCookieValue('react-hookz')); + expect(result.current[0]).toBe(null); + expect(getSpy).toHaveBeenCalledWith('react-hookz'); + }); + + it('should set the cookie value on call to `set`', () => { + const { result } = renderHook(() => useCookieValue('react-hookz')); + + expect(result.current[0]).toBe(null); + act(() => { + result.current[1]('awesome'); + }); + expect(result.current[0]).toBe('awesome'); + expect(setSpy).toHaveBeenCalledWith('react-hookz', 'awesome', {}); + Cookies.remove('react-hookz'); + }); + + it('should remove cookie value on call to `remove`', () => { + const { result } = renderHook(() => useCookieValue('react-hookz')); + + expect(result.current[0]).toBe(null); + act(() => { + result.current[1]('awesome'); + }); + expect(result.current[0]).toBe('awesome'); + + act(() => { + result.current[2](); + }); + expect(result.current[0]).toBe(null); + expect(removeSpy).toHaveBeenCalledWith('react-hookz', {}); + Cookies.remove('react-hookz'); + }); + + it('should re-fetch cookie value on call to `fetch`', () => { + const { result } = renderHook(() => useCookieValue('react-hookz')); + + Cookies.set('react-hookz', 'rulez'); + expect(result.current[0]).toBe(null); + act(() => { + result.current[3](); + }); + expect(result.current[0]).toBe('rulez'); + + Cookies.remove('react-hookz'); + }); + + it('should be synchronized between several hooks with the same key', () => { + const { result: res1 } = renderHook(() => useCookieValue('react-hookz')); + const { result: res2 } = renderHook(() => useCookieValue('react-hookz')); + + expect(res1.current[0]).toBe(null); + expect(res2.current[0]).toBe(null); + + act(() => { + res1.current[1]('awesome'); + }); + + expect(res1.current[0]).toBe('awesome'); + expect(res2.current[0]).toBe('awesome'); + + act(() => { + res2.current[2](); + }); + + expect(res1.current[0]).toBe(null); + expect(res2.current[0]).toBe(null); + }); + + it('should return stable methods', () => { + const { result, rerender } = renderHook(() => useCookieValue('react-hookz')); + + const res1 = result.current; + + rerender(); + + expect(res1[1]).toBe(result.current[1]); + expect(res1[2]).toBe(result.current[2]); + expect(res1[3]).toBe(result.current[3]); + }); }); diff --git a/src/useCookieValue/__tests__/ssr.ts b/src/useCookieValue/__tests__/ssr.ts index b5a4df45b..0dcadaaf5 100644 --- a/src/useCookieValue/__tests__/ssr.ts +++ b/src/useCookieValue/__tests__/ssr.ts @@ -2,17 +2,17 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useCookieValue } from '..'; describe('useCookieValue', () => { - it('should be defined', () => { - expect(useCookieValue).toBeDefined(); - }); + it('should be defined', () => { + expect(useCookieValue).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useCookieValue('react-hookz')); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useCookieValue('react-hookz')); + expect(result.error).toBeUndefined(); + }); - it('should return undefined', () => { - const { result } = renderHook(() => useCookieValue('react-hookz')); - expect(result.current[0]).toBeUndefined(); - }); + it('should return undefined', () => { + const { result } = renderHook(() => useCookieValue('react-hookz')); + expect(result.current[0]).toBeUndefined(); + }); }); diff --git a/src/useCookieValue/index.ts b/src/useCookieValue/index.ts index 4b509a0be..f14345885 100644 --- a/src/useCookieValue/index.ts +++ b/src/useCookieValue/index.ts @@ -8,70 +8,70 @@ import { isBrowser } from '../util/const'; const cookiesSetters = new Map>>(); const registerSetter = (key: string, setter: Dispatch) => { - let setters = cookiesSetters.get(key); + let setters = cookiesSetters.get(key); - if (!setters) { - setters = new Set(); - cookiesSetters.set(key, setters); - } + if (!setters) { + setters = new Set(); + cookiesSetters.set(key, setters); + } - setters.add(setter); + setters.add(setter); }; const unregisterSetter = (key: string, setter: Dispatch): void => { - const setters = cookiesSetters.get(key); + const setters = cookiesSetters.get(key); - // Almost impossible to test in normal situation - /* istanbul ignore next */ - if (!setters) return; + // Almost impossible to test in normal situation + /* istanbul ignore next */ + if (!setters) return; - setters.delete(setter); + setters.delete(setter); - if (!setters.size) { - cookiesSetters.delete(key); - } + if (!setters.size) { + cookiesSetters.delete(key); + } }; const invokeRegisteredSetters = ( - key: string, - value: string | null, - skipSetter?: Dispatch + key: string, + value: string | null, + skipSetter?: Dispatch ) => { - const setters = cookiesSetters.get(key); + const setters = cookiesSetters.get(key); - // Almost impossible to test in normal situation - /* istanbul ignore next */ - if (!setters) return; + // Almost impossible to test in normal situation + /* istanbul ignore next */ + if (!setters) return; - setters.forEach((s) => { - if (s !== skipSetter) s(value); - }); + setters.forEach((s) => { + if (s !== skipSetter) s(value); + }); }; export type UseCookieValueOptions< - InitializeWithValue extends boolean | undefined = boolean | undefined + InitializeWithValue extends boolean | undefined = boolean | undefined > = Cookies.CookieAttributes & - (InitializeWithValue extends undefined - ? { - /** - * Whether to initialize state with the cookie value or `undefined`. - * - * _We suggest setting this to `false` during SSR._ - * - * @default true - */ - initializeWithValue?: InitializeWithValue; - } - : { - initializeWithValue: InitializeWithValue; - }); + (InitializeWithValue extends undefined + ? { + /** + * Whether to initialize state with the cookie value or `undefined`. + * + * _We suggest setting this to `false` during SSR._ + * + * @default true + */ + initializeWithValue?: InitializeWithValue; + } + : { + initializeWithValue: InitializeWithValue; + }); export type UseCookieValueReturn = - [value: V, set: (value: string) => void, remove: () => void, fetch: () => void]; + [value: V, set: (value: string) => void, remove: () => void, fetch: () => void]; export function useCookieValue( - key: string, - options: UseCookieValueOptions + key: string, + options: UseCookieValueOptions ): UseCookieValueReturn; export function useCookieValue(key: string, options?: UseCookieValueOptions): UseCookieValueReturn; /** @@ -81,75 +81,75 @@ export function useCookieValue(key: string, options?: UseCookieValueOptions): Us * @param options Cookie options that will be used during setting and deleting the cookie. */ export function useCookieValue( - key: string, - options: UseCookieValueOptions = {} + key: string, + options: UseCookieValueOptions = {} ): UseCookieValueReturn { - // No need to test it, dev-only notification about 3rd party library requirement - /* istanbul ignore next */ - if (process.env.NODE_ENV === 'development' && Cookies === undefined) { - throw new ReferenceError( - 'Dependency `js-cookies` is not installed, it is required for `useCookieValue` work.' - ); - } - - let { initializeWithValue = true, ...cookiesOptions } = options; - - if (!isBrowser) { - initializeWithValue = false; - } - - const methods = useSyncedRef({ - set(value: string) { - setState(value); - Cookies.set(key, value, cookiesOptions); - // Update all other hooks with the same key - invokeRegisteredSetters(key, value, setState); - }, - remove() { - setState(null); - Cookies.remove(key, cookiesOptions); - invokeRegisteredSetters(key, null, setState); - }, - fetchVal: () => Cookies.get(key) ?? null, - fetch() { - const val = methods.current.fetchVal(); - setState(val); - invokeRegisteredSetters(key, val, setState); - }, - }); - - const isFirstMount = useFirstMountState(); - const [state, setState] = useState( - isFirstMount && initializeWithValue ? methods.current.fetchVal() : undefined - ); - - useMountEffect(() => { - if (!initializeWithValue) { - methods.current.fetch(); - } - }); - - useEffect(() => { - registerSetter(key, setState); - - return () => { - unregisterSetter(key, setState); - }; - }, [key]); - - return [ - state, - - useCallback((value: string) => { - methods.current.set(value); - }, []), - - useCallback(() => { - methods.current.remove(); - }, []), - - useCallback(() => { - methods.current.fetch(); - }, []), - ]; + // No need to test it, dev-only notification about 3rd party library requirement + /* istanbul ignore next */ + if (process.env.NODE_ENV === 'development' && Cookies === undefined) { + throw new ReferenceError( + 'Dependency `js-cookies` is not installed, it is required for `useCookieValue` work.' + ); + } + + let { initializeWithValue = true, ...cookiesOptions } = options; + + if (!isBrowser) { + initializeWithValue = false; + } + + const methods = useSyncedRef({ + set(value: string) { + setState(value); + Cookies.set(key, value, cookiesOptions); + // Update all other hooks with the same key + invokeRegisteredSetters(key, value, setState); + }, + remove() { + setState(null); + Cookies.remove(key, cookiesOptions); + invokeRegisteredSetters(key, null, setState); + }, + fetchVal: () => Cookies.get(key) ?? null, + fetch() { + const val = methods.current.fetchVal(); + setState(val); + invokeRegisteredSetters(key, val, setState); + }, + }); + + const isFirstMount = useFirstMountState(); + const [state, setState] = useState( + isFirstMount && initializeWithValue ? methods.current.fetchVal() : undefined + ); + + useMountEffect(() => { + if (!initializeWithValue) { + methods.current.fetch(); + } + }); + + useEffect(() => { + registerSetter(key, setState); + + return () => { + unregisterSetter(key, setState); + }; + }, [key]); + + return [ + state, + + useCallback((value: string) => { + methods.current.set(value); + }, []), + + useCallback(() => { + methods.current.remove(); + }, []), + + useCallback(() => { + methods.current.fetch(); + }, []), + ]; } diff --git a/src/useCounter/__docs__/example.stories.tsx b/src/useCounter/__docs__/example.stories.tsx index a4142e796..779718c95 100644 --- a/src/useCounter/__docs__/example.stories.tsx +++ b/src/useCounter/__docs__/example.stories.tsx @@ -2,100 +2,100 @@ import React from 'react'; import { useCounter } from '../..'; export function Example() { - const [min, { inc: incMin, dec: decMin }] = useCounter(1); - const [max, { inc: incMax, dec: decMax }] = useCounter(10); - const [value, { inc, dec, set, reset }] = useCounter(5, max, min); + const [min, { inc: incMin, dec: decMin }] = useCounter(1); + const [max, { inc: incMax, dec: decMax }] = useCounter(10); + const [value, { inc, dec, set, reset }] = useCounter(5, max, min); - return ( -
-
- current: {value} [min: {min}; max: {max}] -
-
- Current value: - - - - - - - -
-
- Min value: - - -
-
- Max value: - - -
- ); + return ( +
+
+ current: {value} [min: {min}; max: {max}] +
+
+ Current value: + + + + + + + +
+
+ Min value: + + +
+
+ Max value: + + +
+ ); } diff --git a/src/useCounter/__docs__/story.mdx b/src/useCounter/__docs__/story.mdx index e1a08a5f7..2049580b2 100644 --- a/src/useCounter/__docs__/story.mdx +++ b/src/useCounter/__docs__/story.mdx @@ -13,16 +13,16 @@ Tracks a numeric value and offers functions for manipulating it. #### Example - + ## Reference ```ts export function useCounter( - initialValue: InitialState = 0, - max?: number, - min?: number + initialValue: InitialState = 0, + max?: number, + min?: number ): [number, CounterActions]; ``` diff --git a/src/useCounter/__tests__/dom.ts b/src/useCounter/__tests__/dom.ts index 1b793bee6..2bd1a1038 100644 --- a/src/useCounter/__tests__/dom.ts +++ b/src/useCounter/__tests__/dom.ts @@ -2,231 +2,231 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { useCounter } from '../..'; describe('useCounter', () => { - it('should be defined', () => { - expect(useCounter).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useCounter()); - expect(result.error).toBeUndefined(); - }); - - it('should have default initial value of 0', () => { - const { result } = renderHook(() => useCounter()); - const counter = result.current[0]; - expect(counter).toEqual(0); - }); + it('should be defined', () => { + expect(useCounter).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useCounter()); + expect(result.error).toBeUndefined(); + }); + + it('should have default initial value of 0', () => { + const { result } = renderHook(() => useCounter()); + const counter = result.current[0]; + expect(counter).toEqual(0); + }); - it('should accept custom initial value', () => { - const { result } = renderHook(() => useCounter(5)); - const counter = result.current[0]; - expect(counter).toEqual(5); - }); + it('should accept custom initial value', () => { + const { result } = renderHook(() => useCounter(5)); + const counter = result.current[0]; + expect(counter).toEqual(5); + }); - it('should accept function returning a number as initial value', () => { - const { result } = renderHook(() => useCounter(() => 5)); - const counter = result.current[0]; - expect(counter).toEqual(5); - }); + it('should accept function returning a number as initial value', () => { + const { result } = renderHook(() => useCounter(() => 5)); + const counter = result.current[0]; + expect(counter).toEqual(5); + }); - it('should force initial value to be at least the given minimum value', () => { - const { result } = renderHook(() => useCounter(0, 10, 5)); - const counter = result.current[0]; - expect(counter).toEqual(5); - }); + it('should force initial value to be at least the given minimum value', () => { + const { result } = renderHook(() => useCounter(0, 10, 5)); + const counter = result.current[0]; + expect(counter).toEqual(5); + }); - it('should force initial value to be at most the given maximum value', () => { - const { result } = renderHook(() => useCounter(10, 5)); - const counter = result.current[0]; - expect(counter).toEqual(5); - }); + it('should force initial value to be at most the given maximum value', () => { + const { result } = renderHook(() => useCounter(10, 5)); + const counter = result.current[0]; + expect(counter).toEqual(5); + }); - it('get returns the current counter value', () => { - const { result } = renderHook(() => useCounter(0)); - const { get } = result.current[1]; + it('get returns the current counter value', () => { + const { result } = renderHook(() => useCounter(0)); + const { get } = result.current[1]; - act(() => { - expect(get()).toEqual(result.current[0]); - }); - }); + act(() => { + expect(get()).toEqual(result.current[0]); + }); + }); - it('set sets the counter to any value', () => { - const { result } = renderHook(() => useCounter(0)); - const { set } = result.current[1]; + it('set sets the counter to any value', () => { + const { result } = renderHook(() => useCounter(0)); + const { set } = result.current[1]; - act(() => { - set(2); - }); + act(() => { + set(2); + }); - expect(result.current[0]).toEqual(2); + expect(result.current[0]).toEqual(2); - act(() => { - set((current: number) => current + 5); - }); + act(() => { + set((current: number) => current + 5); + }); - expect(result.current[0]).toEqual(7); + expect(result.current[0]).toEqual(7); - act(() => { - set(12); - }); + act(() => { + set(12); + }); - expect(result.current[0]).toEqual(12); - }); + expect(result.current[0]).toEqual(12); + }); - it('set respects min and max parameters', () => { - const { result } = renderHook(() => useCounter(0, 10, 0)); - const { set } = result.current[1]; + it('set respects min and max parameters', () => { + const { result } = renderHook(() => useCounter(0, 10, 0)); + const { set } = result.current[1]; - act(() => { - set(-2); - }); + act(() => { + set(-2); + }); - expect(result.current[0]).toEqual(0); + expect(result.current[0]).toEqual(0); - act(() => { - set(12); - }); + act(() => { + set(12); + }); - expect(result.current[0]).toEqual(10); - }); + expect(result.current[0]).toEqual(10); + }); - it('inc increments the counter by 1 if no delta given', () => { - const { result } = renderHook(() => useCounter(0)); - const { inc } = result.current[1]; + it('inc increments the counter by 1 if no delta given', () => { + const { result } = renderHook(() => useCounter(0)); + const { inc } = result.current[1]; - act(() => { - inc(); - }); + act(() => { + inc(); + }); - const counter = result.current[0]; - expect(counter).toEqual(1); - }); + const counter = result.current[0]; + expect(counter).toEqual(1); + }); - it('inc increments the counter by the given delta', () => { - const { result } = renderHook(() => useCounter(0)); - const { inc } = result.current[1]; + it('inc increments the counter by the given delta', () => { + const { result } = renderHook(() => useCounter(0)); + const { inc } = result.current[1]; - act(() => { - inc(2); - }); + act(() => { + inc(2); + }); - expect(result.current[0]).toEqual(2); + expect(result.current[0]).toEqual(2); - act(() => { - inc((current) => current + 1); - }); + act(() => { + inc((current) => current + 1); + }); - expect(result.current[0]).toEqual(5); - }); + expect(result.current[0]).toEqual(5); + }); - it('inc respects min and max parameters', () => { - const { result } = renderHook(() => useCounter(0, 5, 0)); - const { inc } = result.current[1]; + it('inc respects min and max parameters', () => { + const { result } = renderHook(() => useCounter(0, 5, 0)); + const { inc } = result.current[1]; - act(() => { - inc(-2); - }); + act(() => { + inc(-2); + }); - expect(result.current[0]).toEqual(0); + expect(result.current[0]).toEqual(0); - act(() => { - inc(12); - }); + act(() => { + inc(12); + }); - expect(result.current[0]).toEqual(5); - }); + expect(result.current[0]).toEqual(5); + }); - it('dec decrements the counter by 1 if no delta given', () => { - const { result } = renderHook(() => useCounter(0)); - const { dec } = result.current[1]; + it('dec decrements the counter by 1 if no delta given', () => { + const { result } = renderHook(() => useCounter(0)); + const { dec } = result.current[1]; - act(() => { - dec(); - }); + act(() => { + dec(); + }); - const counter = result.current[0]; - expect(counter).toEqual(-1); - }); + const counter = result.current[0]; + expect(counter).toEqual(-1); + }); - it('dec decrements the counter by the given delta', () => { - const { result } = renderHook(() => useCounter(0)); - const { dec } = result.current[1]; + it('dec decrements the counter by the given delta', () => { + const { result } = renderHook(() => useCounter(0)); + const { dec } = result.current[1]; - act(() => { - dec(2); - }); + act(() => { + dec(2); + }); - expect(result.current[0]).toEqual(-2); + expect(result.current[0]).toEqual(-2); - act(() => { - dec((current) => current + 1); - }); + act(() => { + dec((current) => current + 1); + }); - expect(result.current[0]).toEqual(-1); - }); + expect(result.current[0]).toEqual(-1); + }); - it('dec respects min and max parameters', () => { - const { result } = renderHook(() => useCounter(0, 5, 0)); - const { dec } = result.current[1]; + it('dec respects min and max parameters', () => { + const { result } = renderHook(() => useCounter(0, 5, 0)); + const { dec } = result.current[1]; - act(() => { - dec(2); - }); + act(() => { + dec(2); + }); - expect(result.current[0]).toEqual(0); + expect(result.current[0]).toEqual(0); - act(() => { - dec(-12); - }); + act(() => { + dec(-12); + }); - expect(result.current[0]).toEqual(5); - }); + expect(result.current[0]).toEqual(5); + }); - it('reset without arguments sets the counter to its initial value', () => { - const { result } = renderHook(() => useCounter(0)); - const { reset, inc } = result.current[1]; + it('reset without arguments sets the counter to its initial value', () => { + const { result } = renderHook(() => useCounter(0)); + const { reset, inc } = result.current[1]; - act(() => { - inc(); - reset(); - }); + act(() => { + inc(); + reset(); + }); - expect(result.current[0]).toEqual(0); - }); + expect(result.current[0]).toEqual(0); + }); - it('reset with argument sets the counter to its new initial value', () => { - const { result } = renderHook(() => useCounter(0)); - const { reset, inc } = result.current[1]; + it('reset with argument sets the counter to its new initial value', () => { + const { result } = renderHook(() => useCounter(0)); + const { reset, inc } = result.current[1]; - act(() => { - inc(); - reset(5); - }); + act(() => { + inc(); + reset(5); + }); - expect(result.current[0]).toEqual(5); + expect(result.current[0]).toEqual(5); - act(() => { - inc(); - reset(); - }); + act(() => { + inc(); + reset(); + }); - expect(result.current[0]).toEqual(0); - }); + expect(result.current[0]).toEqual(0); + }); - it('reset respects min and max parameters', () => { - const { result } = renderHook(() => useCounter(0, 10, 0)); - const { reset } = result.current[1]; + it('reset respects min and max parameters', () => { + const { result } = renderHook(() => useCounter(0, 10, 0)); + const { reset } = result.current[1]; - act(() => { - reset(25); - }); + act(() => { + reset(25); + }); - expect(result.current[0]).toEqual(10); + expect(result.current[0]).toEqual(10); - act(() => { - reset(-10); - }); + act(() => { + reset(-10); + }); - expect(result.current[0]).toEqual(0); - }); + expect(result.current[0]).toEqual(0); + }); }); diff --git a/src/useCounter/__tests__/ssr.ts b/src/useCounter/__tests__/ssr.ts index 95e2a6fe5..3aef0deba 100644 --- a/src/useCounter/__tests__/ssr.ts +++ b/src/useCounter/__tests__/ssr.ts @@ -2,12 +2,12 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useCounter } from '../..'; describe('useCounter', () => { - it('should be defined', () => { - expect(useCounter).toBeDefined(); - }); + it('should be defined', () => { + expect(useCounter).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useCounter()); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useCounter()); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useCounter/index.ts b/src/useCounter/index.ts index 00227a456..6fc8d6569 100644 --- a/src/useCounter/index.ts +++ b/src/useCounter/index.ts @@ -4,37 +4,37 @@ import { useSyncedRef } from '../useSyncedRef'; import { type InitialState, resolveHookState } from '../util/resolveHookState'; export type CounterActions = { - /** - * Returns the current value of the counter. - */ - get: () => number; - /** - * Increment the counter by the given `delta`. - * - * @param `delta` number or function returning a number. By default, `delta` is 1. - */ - inc: (delta?: SetStateAction) => void; - /** - * Decrement the counter by the given `delta`. - * - * @param `delta` number or function returning a number. By default, `delta` is 1. - */ - dec: (delta?: SetStateAction) => void; - /** - * Set the counter to any value, limited only by the `min` and `max` parameters of the hook. - * - * @param `value` number or function returning a number - */ - set: (value: SetStateAction) => void; - /** - * Resets the counter to its original initial value. - * - * If `value` is given, then it becomes the new initial value of the hook and - * following calls to `reset` without arguments will reset the counter to `value`. - * - * @param `value` number or function returning a number - */ - reset: (value?: SetStateAction) => void; + /** + * Returns the current value of the counter. + */ + get: () => number; + /** + * Increment the counter by the given `delta`. + * + * @param `delta` number or function returning a number. By default, `delta` is 1. + */ + inc: (delta?: SetStateAction) => void; + /** + * Decrement the counter by the given `delta`. + * + * @param `delta` number or function returning a number. By default, `delta` is 1. + */ + dec: (delta?: SetStateAction) => void; + /** + * Set the counter to any value, limited only by the `min` and `max` parameters of the hook. + * + * @param `value` number or function returning a number + */ + set: (value: SetStateAction) => void; + /** + * Resets the counter to its original initial value. + * + * If `value` is given, then it becomes the new initial value of the hook and + * following calls to `reset` without arguments will reset the counter to `value`. + * + * @param `value` number or function returning a number + */ + reset: (value?: SetStateAction) => void; }; /** @@ -47,40 +47,40 @@ export type CounterActions = { * If `initialValue` is smaller than `min`, then `min` is set as the initial value. */ export function useCounter( - initialValue: InitialState = 0, - max?: number, - min?: number + initialValue: InitialState = 0, + max?: number, + min?: number ): [number, CounterActions] { - const [state, setState] = useMediatedState(initialValue, (v: number): number => { - if (max !== undefined) { - v = Math.min(max, v); - } + const [state, setState] = useMediatedState(initialValue, (v: number): number => { + if (max !== undefined) { + v = Math.min(max, v); + } - if (min !== undefined) { - v = Math.max(min, v); - } + if (min !== undefined) { + v = Math.max(min, v); + } - return v; - }); - const stateRef = useSyncedRef(state); + return v; + }); + const stateRef = useSyncedRef(state); - return [ - state, - useMemo( - () => ({ - get: () => stateRef.current, - set: setState, - dec(delta = 1) { - setState((val) => val - resolveHookState(delta, val)); - }, - inc(delta = 1) { - setState((val) => val + resolveHookState(delta, val)); - }, - reset(val = initialValue) { - setState((v) => resolveHookState(val, v)); - }, - }), - [initialValue, setState, stateRef] - ), - ]; + return [ + state, + useMemo( + () => ({ + get: () => stateRef.current, + set: setState, + dec(delta = 1) { + setState((val) => val - resolveHookState(delta, val)); + }, + inc(delta = 1) { + setState((val) => val + resolveHookState(delta, val)); + }, + reset(val = initialValue) { + setState((v) => resolveHookState(val, v)); + }, + }), + [initialValue, setState, stateRef] + ), + ]; } diff --git a/src/useCustomCompareEffect/__docs__/example.stories.tsx b/src/useCustomCompareEffect/__docs__/example.stories.tsx index 8ca84eab6..4856bbe8d 100644 --- a/src/useCustomCompareEffect/__docs__/example.stories.tsx +++ b/src/useCustomCompareEffect/__docs__/example.stories.tsx @@ -6,106 +6,106 @@ import { useCustomCompareEffect, useUpdateEffect } from '../..'; * @see https://stackoverflow.com/a/52171480/7304377 */ const hashWithCYRB53 = (someString: string) => { - let h1 = 0xde_ad_be_ef; - let h2 = 0x41_c6_ce_57; + let h1 = 0xde_ad_be_ef; + let h2 = 0x41_c6_ce_57; - /* eslint-disable no-bitwise */ - for (let i = 0, ch; i < someString.length; i++) { - ch = someString.codePointAt(i) ?? 0; - h1 = Math.imul(h1 ^ ch, 2_654_435_761); - h2 = Math.imul(h2 ^ ch, 1_597_334_677); - } + /* eslint-disable no-bitwise */ + for (let i = 0, ch; i < someString.length; i++) { + ch = someString.codePointAt(i) ?? 0; + h1 = Math.imul(h1 ^ ch, 2_654_435_761); + h2 = Math.imul(h2 ^ ch, 1_597_334_677); + } - h1 = Math.imul(h1 ^ (h1 >>> 16), 2_246_822_507) ^ Math.imul(h2 ^ (h2 >>> 13), 3_266_489_909); - h2 = Math.imul(h2 ^ (h2 >>> 16), 2_246_822_507) ^ Math.imul(h1 ^ (h1 >>> 13), 3_266_489_909); + h1 = Math.imul(h1 ^ (h1 >>> 16), 2_246_822_507) ^ Math.imul(h2 ^ (h2 >>> 13), 3_266_489_909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2_246_822_507) ^ Math.imul(h1 ^ (h1 >>> 13), 3_266_489_909); - return 4_294_967_296 * (2_097_151 & h2) + (h1 >>> 0); - /* eslint-enable no-bitwise */ + return 4_294_967_296 * (2_097_151 & h2) + (h1 >>> 0); + /* eslint-enable no-bitwise */ }; export function Example() { - const [valueA, setValueA] = React.useState(0); - const [valueB, setValueB] = React.useState(0); - const [irrelevantValue, setIrrelevantValue] = React.useState(0); + const [valueA, setValueA] = React.useState(0); + const [valueB, setValueB] = React.useState(0); + const [irrelevantValue, setIrrelevantValue] = React.useState(0); - const incrementValueA = () => { - setValueA((prev) => prev + 1); - }; + const incrementValueA = () => { + setValueA((prev) => prev + 1); + }; - const incrementValueB = () => { - setValueB((prev) => prev + 1); - }; + const incrementValueB = () => { + setValueB((prev) => prev + 1); + }; - const incrementIrrelevantValue = () => { - setIrrelevantValue((prev) => prev + 1); - }; + const incrementIrrelevantValue = () => { + setIrrelevantValue((prev) => prev + 1); + }; - const objectA = { key: valueA }; - const objectB = { key: valueB }; + const objectA = { key: valueA }; + const objectB = { key: valueB }; - useCustomCompareEffect( - () => { - // eslint-disable-next-line no-alert - window.alert('Detected checksum difference from previous render!'); - }, - [objectA, objectB], - (a, b) => hashWithCYRB53(JSON.stringify(a)) === hashWithCYRB53(JSON.stringify(b)), - useUpdateEffect - ); + useCustomCompareEffect( + () => { + // eslint-disable-next-line no-alert + window.alert('Detected checksum difference from previous render!'); + }, + [objectA, objectB], + (a, b) => hashWithCYRB53(JSON.stringify(a)) === hashWithCYRB53(JSON.stringify(b)), + useUpdateEffect + ); - return ( -
-

- In this example, there exist two objects in memory that are initialized identically. There - is an alert that only appears when the objects differ. You can press either button to adjust - each object's only value. We hash the objects and use those hashes as a checksum to - determine if the objects have changed. This is ridiculous for the objects here (because - they're tiny and simple), but if you have potentially large, complex objects you may - wish to go this route instead of leveraging some sort of deep equality check. -

-

- We're also using `useUpdateEffect` instead of `useEffect` to avoid running the effect - on the initial mount of the component. -

+ return ( +
+

+ In this example, there exist two objects in memory that are initialized identically. There + is an alert that only appears when the objects differ. You can press either button to adjust + each object's only value. We hash the objects and use those hashes as a checksum to + determine if the objects have changed. This is ridiculous for the objects here (because + they're tiny and simple), but if you have potentially large, complex objects you may + wish to go this route instead of leveraging some sort of deep equality check. +

+

+ We're also using `useUpdateEffect` instead of `useEffect` to avoid running the effect + on the initial mount of the component. +

- + -
-
- Current valueA: {valueA}{' '} - -
-
- Current valueB: {valueB}{' '} - -
-
- Current irrelevantValue: {irrelevantValue}{' '} - -
+
+
+ Current valueA: {valueA}{' '} + +
+
+ Current valueB: {valueB}{' '} + +
+
+ Current irrelevantValue: {irrelevantValue}{' '} + +
- Current hash objectA: {hashWithCYRB53(JSON.stringify(objectA))} - Current hash objectB: {hashWithCYRB53(JSON.stringify(objectB))} -
-
- ); + Current hash objectA: {hashWithCYRB53(JSON.stringify(objectA))} + Current hash objectB: {hashWithCYRB53(JSON.stringify(objectB))} +
+
+ ); } diff --git a/src/useCustomCompareEffect/__docs__/story.mdx b/src/useCustomCompareEffect/__docs__/story.mdx index ea38d805f..f0fff0d27 100644 --- a/src/useCustomCompareEffect/__docs__/story.mdx +++ b/src/useCustomCompareEffect/__docs__/story.mdx @@ -14,23 +14,23 @@ Like `useEffect` but uses provided comparator function to validate dependency ch #### Example - + ## Reference ```ts export function useCustomCompareEffect< - Callback extends EffectCallback = EffectCallback, - Deps extends DependencyList = DependencyList, - HookRestArgs extends any[] = any[], - R extends HookRestArgs = HookRestArgs + Callback extends EffectCallback = EffectCallback, + Deps extends DependencyList = DependencyList, + HookRestArgs extends any[] = any[], + R extends HookRestArgs = HookRestArgs >( - callback: Callback, - deps: Deps, - comparator: DependenciesComparator = basicDepsComparator, - effectHook: EffectHook = useEffect, - ...effectHookRestArgs: R + callback: Callback, + deps: Deps, + comparator: DependenciesComparator = basicDepsComparator, + effectHook: EffectHook = useEffect, + ...effectHookRestArgs: R ): void; ``` diff --git a/src/useCustomCompareEffect/__tests__/dom.ts b/src/useCustomCompareEffect/__tests__/dom.ts index 982ed1de3..51ab37eb1 100644 --- a/src/useCustomCompareEffect/__tests__/dom.ts +++ b/src/useCustomCompareEffect/__tests__/dom.ts @@ -3,74 +3,74 @@ import { type DependencyList } from 'react'; import { type EffectCallback, useCustomCompareEffect, useUpdateEffect } from '../..'; describe('useCustomCompareEffect', () => { - it('should be defined', () => { - expect(useCustomCompareEffect).toBeDefined(); - }); + it('should be defined', () => { + expect(useCustomCompareEffect).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useCustomCompareEffect(() => {}, []); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useCustomCompareEffect(() => {}, []); + }); + expect(result.error).toBeUndefined(); + }); - it('should not call provided comparator on render', () => { - const spy = jest.fn(); - renderHook(() => { - useCustomCompareEffect(() => {}, [], spy, useUpdateEffect); - }); - expect(spy).toHaveBeenCalledTimes(0); - }); + it('should not call provided comparator on render', () => { + const spy = jest.fn(); + renderHook(() => { + useCustomCompareEffect(() => {}, [], spy, useUpdateEffect); + }); + expect(spy).toHaveBeenCalledTimes(0); + }); - it('should call comparator with previous and current deps as args', () => { - const spy = jest.fn(); - const { rerender } = renderHook( - ({ deps }) => { - useCustomCompareEffect(() => {}, deps, spy, useUpdateEffect); - }, - { initialProps: { deps: [1, 2] } } - ); - rerender({ deps: [1, 3] }); + it('should call comparator with previous and current deps as args', () => { + const spy = jest.fn(); + const { rerender } = renderHook( + ({ deps }) => { + useCustomCompareEffect(() => {}, deps, spy, useUpdateEffect); + }, + { initialProps: { deps: [1, 2] } } + ); + rerender({ deps: [1, 3] }); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy.mock.calls[0][0]).toStrictEqual([1, 2]); - expect(spy.mock.calls[0][1]).toStrictEqual([1, 3]); - }); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy.mock.calls[0][0]).toStrictEqual([1, 2]); + expect(spy.mock.calls[0][1]).toStrictEqual([1, 3]); + }); - it('should not pass new deps to underlying effect only if comparator reported unequal deps', () => { - const spy = jest.fn(useUpdateEffect); - const { rerender } = renderHook( - ({ deps }) => { - useCustomCompareEffect(() => {}, deps, undefined, spy); - }, - { initialProps: { deps: [1, 2] } } - ); - rerender({ deps: [1, 2] }); + it('should not pass new deps to underlying effect only if comparator reported unequal deps', () => { + const spy = jest.fn(useUpdateEffect); + const { rerender } = renderHook( + ({ deps }) => { + useCustomCompareEffect(() => {}, deps, undefined, spy); + }, + { initialProps: { deps: [1, 2] } } + ); + rerender({ deps: [1, 2] }); - expect(spy).toHaveBeenCalledTimes(2); - expect(spy.mock.calls[0][1]).toStrictEqual([1, 2]); - expect(spy.mock.calls[0][1]).toBe(spy.mock.calls[1][1]); + expect(spy).toHaveBeenCalledTimes(2); + expect(spy.mock.calls[0][1]).toStrictEqual([1, 2]); + expect(spy.mock.calls[0][1]).toBe(spy.mock.calls[1][1]); - rerender({ deps: [1, 3] }); + rerender({ deps: [1, 3] }); - expect(spy).toHaveBeenCalledTimes(3); - expect(spy.mock.calls[2][1]).toStrictEqual([1, 3]); - expect(spy.mock.calls[0][1]).not.toBe(spy.mock.calls[2][1]); - }); + expect(spy).toHaveBeenCalledTimes(3); + expect(spy.mock.calls[2][1]).toStrictEqual([1, 3]); + expect(spy.mock.calls[0][1]).not.toBe(spy.mock.calls[2][1]); + }); - it('should pass res argument to underlying hook', () => { - const spy = jest.fn((c: EffectCallback, d: DependencyList, _n: number) => { - useUpdateEffect(c, d); - }); - renderHook( - ({ deps }) => { - useCustomCompareEffect(() => {}, deps, undefined, spy, 123); - }, - { - initialProps: { deps: [1, 2] }, - } - ); + it('should pass res argument to underlying hook', () => { + const spy = jest.fn((c: EffectCallback, d: DependencyList, _n: number) => { + useUpdateEffect(c, d); + }); + renderHook( + ({ deps }) => { + useCustomCompareEffect(() => {}, deps, undefined, spy, 123); + }, + { + initialProps: { deps: [1, 2] }, + } + ); - expect(spy.mock.calls[0][2]).toBe(123); - }); + expect(spy.mock.calls[0][2]).toBe(123); + }); }); diff --git a/src/useCustomCompareEffect/__tests__/ssr.ts b/src/useCustomCompareEffect/__tests__/ssr.ts index e8575ca02..d3e73c558 100644 --- a/src/useCustomCompareEffect/__tests__/ssr.ts +++ b/src/useCustomCompareEffect/__tests__/ssr.ts @@ -2,22 +2,22 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useCustomCompareEffect } from '../..'; describe('useCustomCompareEffect', () => { - it('should be defined', () => { - expect(useCustomCompareEffect).toBeDefined(); - }); + it('should be defined', () => { + expect(useCustomCompareEffect).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useCustomCompareEffect(() => {}, []); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useCustomCompareEffect(() => {}, []); + }); + expect(result.error).toBeUndefined(); + }); - it('should not invoke comparator', () => { - const spy = jest.fn(); - renderHook(() => { - useCustomCompareEffect(() => {}, [], spy); - }); - expect(spy).not.toHaveBeenCalled(); - }); + it('should not invoke comparator', () => { + const spy = jest.fn(); + renderHook(() => { + useCustomCompareEffect(() => {}, [], spy); + }); + expect(spy).not.toHaveBeenCalled(); + }); }); diff --git a/src/useCustomCompareEffect/index.ts b/src/useCustomCompareEffect/index.ts index dcdb2f9ad..151e04d38 100644 --- a/src/useCustomCompareEffect/index.ts +++ b/src/useCustomCompareEffect/index.ts @@ -18,26 +18,26 @@ import { basicDepsComparator, type EffectCallback, type EffectHook } from '../ut */ // eslint-disable-next-line max-params export function useCustomCompareEffect< - Callback extends EffectCallback = EffectCallback, - Deps extends DependencyList = DependencyList, - HookRestArgs extends any[] = any[], - R extends HookRestArgs = HookRestArgs + Callback extends EffectCallback = EffectCallback, + Deps extends DependencyList = DependencyList, + HookRestArgs extends any[] = any[], + R extends HookRestArgs = HookRestArgs >( - callback: Callback, - deps: Deps, - comparator: DependenciesComparator = basicDepsComparator, - effectHook: EffectHook = useEffect, - ...effectHookRestArgs: R + callback: Callback, + deps: Deps, + comparator: DependenciesComparator = basicDepsComparator, + effectHook: EffectHook = useEffect, + ...effectHookRestArgs: R ): void { - const dependencies = useRef(); + const dependencies = useRef(); - // Effects are not run during SSR, therefore, it makes no sense to invoke the comparator - if ( - dependencies.current === undefined || - (isBrowser && !comparator(dependencies.current, deps)) - ) { - dependencies.current = deps; - } + // Effects are not run during SSR, therefore, it makes no sense to invoke the comparator + if ( + dependencies.current === undefined || + (isBrowser && !comparator(dependencies.current, deps)) + ) { + dependencies.current = deps; + } - effectHook(callback, dependencies.current, ...effectHookRestArgs); + effectHook(callback, dependencies.current, ...effectHookRestArgs); } diff --git a/src/useCustomCompareMemo/__docs__/example.stories.tsx b/src/useCustomCompareMemo/__docs__/example.stories.tsx index 7f258a720..8cd368279 100644 --- a/src/useCustomCompareMemo/__docs__/example.stories.tsx +++ b/src/useCustomCompareMemo/__docs__/example.stories.tsx @@ -6,56 +6,56 @@ const keys = ['firstname', 'name']; // Utils const getRandom = (array: ArrayParam) => - array[Math.floor(Math.random() * array.length)] as ArrayParam[number]; + array[Math.floor(Math.random() * array.length)] as ArrayParam[number]; const displayAsJSON = (object: Record) => JSON.stringify(object, undefined, 2); const reverse = (object: Record) => - Object.fromEntries(Object.entries(object).map(([key, value]) => [value, key])); + Object.fromEntries(Object.entries(object).map(([key, value]) => [value, key])); // Hooks const useForce = () => useReducer((state) => !state, false)[1]; export function Example() { - const force = useForce(); - - const reversedPerson = useRef | null>(null); - - const memoCalls = useRef(0); - const retainCalls = useRef(0); - - // eslint-disable-next-line react-hooks/exhaustive-deps - const person = { key: getRandom(keys), value: 'John' }; - - useCustomCompareMemo( - () => { - if (person) { - retainCalls.current++; - - reversedPerson.current = reverse(person); - } - }, - [person] as const, - (savedDeps, deps) => { - const savedDependency = savedDeps[0]; - const dependency = deps[0]; - - return savedDependency.key !== dependency.key; - } - ); - - useMemo(() => { - if (person) { - memoCalls.current++; - } - }, [person]); - - return ( -
- {person && displayAsJSON(person)} -

memo calls: {memoCalls.current}

-

custom memo calls: {retainCalls.current}

- -
- ); + const force = useForce(); + + const reversedPerson = useRef | null>(null); + + const memoCalls = useRef(0); + const retainCalls = useRef(0); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const person = { key: getRandom(keys), value: 'John' }; + + useCustomCompareMemo( + () => { + if (person) { + retainCalls.current++; + + reversedPerson.current = reverse(person); + } + }, + [person] as const, + (savedDeps, deps) => { + const savedDependency = savedDeps[0]; + const dependency = deps[0]; + + return savedDependency.key !== dependency.key; + } + ); + + useMemo(() => { + if (person) { + memoCalls.current++; + } + }, [person]); + + return ( +
+ {person && displayAsJSON(person)} +

memo calls: {memoCalls.current}

+

custom memo calls: {retainCalls.current}

+ +
+ ); } diff --git a/src/useCustomCompareMemo/__docs__/story.mdx b/src/useCustomCompareMemo/__docs__/story.mdx index aa16e43c3..e3ad2640c 100644 --- a/src/useCustomCompareMemo/__docs__/story.mdx +++ b/src/useCustomCompareMemo/__docs__/story.mdx @@ -11,21 +11,21 @@ Like useMemo but uses provided comparator function to validate dependency change #### Example - + ## Reference ```ts export type DependenciesComparator = ( - a: Deps, - b: Deps + a: Deps, + b: Deps ) => boolean; function useCustomCompareMemo( - factory: () => T, - deps: Deps, - comparator: DependenciesComparator + factory: () => T, + deps: Deps, + comparator: DependenciesComparator ): T; ``` diff --git a/src/useCustomCompareMemo/__tests__/dom.ts b/src/useCustomCompareMemo/__tests__/dom.ts index f1565e2df..7c29ac601 100644 --- a/src/useCustomCompareMemo/__tests__/dom.ts +++ b/src/useCustomCompareMemo/__tests__/dom.ts @@ -6,58 +6,58 @@ const mockUser = { name: 'John' }; type User = typeof mockUser; describe('useCustomCompareMemo', () => { - it('should be defined', () => { - expect(useCustomCompareMemo).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => - useCustomCompareMemo( - () => mockUser, - [], - () => true - ) - ); - - expect(result.error).toBeUndefined(); - }); - - it(`should't invoke factory function on each rerender`, () => { - type Props = { user: User }; - const { result, rerender } = renderHook( - ({ user }: Props) => - useCustomCompareMemo( - () => user, - [user], - () => true - ), - { initialProps: { user: mockUser } } - ); - - rerender({ user: { name: 'Jack' } }); - - expect(result.current).toBe(mockUser); - }); - - it('should invoke factory function when user name is not the same', () => { - type Props = { user: User }; - const { result, rerender } = renderHook( - ({ user }: Props) => - useCustomCompareMemo( - () => user, - [user], - (savedDeps, deps) => savedDeps[0].name === deps[0].name - ), - { initialProps: { user: mockUser } } - ); - - rerender({ user: { name: 'John' } }); - - expect(result.current).toBe(mockUser); - - const newUser = { name: 'Mike' }; - rerender({ user: newUser }); - - expect(result.current).toBe(newUser); - }); + it('should be defined', () => { + expect(useCustomCompareMemo).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => + useCustomCompareMemo( + () => mockUser, + [], + () => true + ) + ); + + expect(result.error).toBeUndefined(); + }); + + it(`should't invoke factory function on each rerender`, () => { + type Props = { user: User }; + const { result, rerender } = renderHook( + ({ user }: Props) => + useCustomCompareMemo( + () => user, + [user], + () => true + ), + { initialProps: { user: mockUser } } + ); + + rerender({ user: { name: 'Jack' } }); + + expect(result.current).toBe(mockUser); + }); + + it('should invoke factory function when user name is not the same', () => { + type Props = { user: User }; + const { result, rerender } = renderHook( + ({ user }: Props) => + useCustomCompareMemo( + () => user, + [user], + (savedDeps, deps) => savedDeps[0].name === deps[0].name + ), + { initialProps: { user: mockUser } } + ); + + rerender({ user: { name: 'John' } }); + + expect(result.current).toBe(mockUser); + + const newUser = { name: 'Mike' }; + rerender({ user: newUser }); + + expect(result.current).toBe(newUser); + }); }); diff --git a/src/useCustomCompareMemo/__tests__/ssr.ts b/src/useCustomCompareMemo/__tests__/ssr.ts index bd48080ce..251609e33 100644 --- a/src/useCustomCompareMemo/__tests__/ssr.ts +++ b/src/useCustomCompareMemo/__tests__/ssr.ts @@ -2,18 +2,18 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useCustomCompareMemo } from '../..'; describe('useCustomCompareMemo', () => { - it('should be defined', () => { - expect(useCustomCompareMemo).toBeDefined(); - }); + it('should be defined', () => { + expect(useCustomCompareMemo).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => - useCustomCompareMemo( - () => ({ user: { name: 'John' } }), - [], - () => true - ) - ); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => + useCustomCompareMemo( + () => ({ user: { name: 'John' } }), + [], + () => true + ) + ); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useCustomCompareMemo/index.ts b/src/useCustomCompareMemo/index.ts index 4c7f086ef..73d5088a8 100644 --- a/src/useCustomCompareMemo/index.ts +++ b/src/useCustomCompareMemo/index.ts @@ -11,15 +11,15 @@ import type { DependenciesComparator } from '../types'; * @returns useMemo result */ export const useCustomCompareMemo = ( - factory: () => T, - deps: Deps, - comparator: DependenciesComparator + factory: () => T, + deps: Deps, + comparator: DependenciesComparator ): T => { - const dependencies = useRef(); + const dependencies = useRef(); - if (dependencies.current === undefined || !comparator(dependencies.current, deps)) { - dependencies.current = deps; - } + if (dependencies.current === undefined || !comparator(dependencies.current, deps)) { + dependencies.current = deps; + } - return useMemo(factory, dependencies.current); + return useMemo(factory, dependencies.current); }; diff --git a/src/useDebouncedCallback/__docs__/example.stories.tsx b/src/useDebouncedCallback/__docs__/example.stories.tsx index ed74cc35d..606b90439 100644 --- a/src/useDebouncedCallback/__docs__/example.stories.tsx +++ b/src/useDebouncedCallback/__docs__/example.stories.tsx @@ -2,25 +2,25 @@ import React, { type ComponentProps, useState } from 'react'; import { useDebouncedCallback } from '../..'; export function Example() { - const [state, setState] = useState(''); + const [state, setState] = useState(''); - const handleChange: React.ChangeEventHandler = useDebouncedCallback< - NonNullable['onChange']> - >( - (ev) => { - setState(ev.target.value); - }, - [], - 300, - 500 - ); + const handleChange: React.ChangeEventHandler = useDebouncedCallback< + NonNullable['onChange']> + >( + (ev) => { + setState(ev.target.value); + }, + [], + 300, + 500 + ); - return ( -
-
Below state will update 300ms after last change, but at least once every 500ms
-
-
The input`s value is: {state}
- -
- ); + return ( +
+
Below state will update 300ms after last change, but at least once every 500ms
+
+
The input`s value is: {state}
+ +
+ ); } diff --git a/src/useDebouncedCallback/__docs__/story.mdx b/src/useDebouncedCallback/__docs__/story.mdx index 8687425fc..b1a110322 100644 --- a/src/useDebouncedCallback/__docs__/story.mdx +++ b/src/useDebouncedCallback/__docs__/story.mdx @@ -20,17 +20,17 @@ Deferred execution automatically cancelled on component unmount. #### Example - + ## Reference ```ts export function useDebouncedCallback( - callback: (this: This, ...args: Args) => any, - deps: DependencyList, - delay: number, - maxWait = 0 + callback: (this: This, ...args: Args) => any, + deps: DependencyList, + delay: number, + maxWait = 0 ): DebouncedFunction; ``` diff --git a/src/useDebouncedCallback/__tests__/dom.ts b/src/useDebouncedCallback/__tests__/dom.ts index 837b7ed4b..1c1f03825 100644 --- a/src/useDebouncedCallback/__tests__/dom.ts +++ b/src/useDebouncedCallback/__tests__/dom.ts @@ -2,154 +2,154 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useDebouncedCallback } from '../..'; describe('useDebouncedCallback', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.clearAllTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - it('should be defined', () => { - expect(useDebouncedCallback).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => { - useDebouncedCallback(() => {}, [], 200); - }); - expect(result.error).toBeUndefined(); - }); - - it('should return function same length and wrapped name', () => { - let { result } = renderHook(() => - useDebouncedCallback((_a: any, _b: any, _c: any) => {}, [], 200) - ); - - expect(result.current.length).toBe(3); - expect(result.current.name).toBe(`anonymous__debounced__200`); - - function testFn(_a: any, _b: any, _c: any) {} - - result = renderHook(() => useDebouncedCallback(testFn, [], 100)).result; - - expect(result.current.length).toBe(3); - expect(result.current.name).toBe(`testFn__debounced__100`); - }); - - it('should return new callback if delay is changed', () => { - const { result, rerender } = renderHook( - ({ delay }) => useDebouncedCallback(() => {}, [], delay), - { - initialProps: { delay: 200 }, - } - ); - - const cb1 = result.current; - rerender({ delay: 123 }); - - expect(cb1).not.toBe(result.current); - }); - - it('should run given callback only after specified delay since last call', () => { - const cb = jest.fn(); - const { result } = renderHook(() => useDebouncedCallback(cb, [], 200)); - - result.current(); - expect(cb).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(100); - result.current(); - - jest.advanceTimersByTime(199); - expect(cb).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(1); - expect(cb).toHaveBeenCalledTimes(1); - }); - - it('should pass parameters to callback', () => { - const cb = jest.fn((_a: number, _c: string) => {}); - const { result } = renderHook(() => useDebouncedCallback(cb, [], 200)); - - result.current(1, 'abc'); - jest.advanceTimersByTime(200); - expect(cb).toHaveBeenCalledWith(1, 'abc'); - }); - - it('should cancel previously scheduled call even if parameters changed', () => { - const cb1 = jest.fn(() => {}); - const cb2 = jest.fn(() => {}); - - const { result, rerender } = renderHook( - ({ i }) => - useDebouncedCallback( - () => { - if (i === 1) { - cb1(); - } else { - cb2(); - } - }, - [i], - 200 - ), - { initialProps: { i: 1 } } - ); - - result.current(); - jest.advanceTimersByTime(100); - - rerender({ i: 2 }); - result.current(); - jest.advanceTimersByTime(200); - - expect(cb1).not.toHaveBeenCalled(); - expect(cb2).toHaveBeenCalledTimes(1); - }); - - it('should cancel debounce execution after component unmount', () => { - const cb = jest.fn(); - - const { result, unmount } = renderHook(() => useDebouncedCallback(cb, [], 150, 200)); - - result.current(); - expect(cb).not.toHaveBeenCalled(); - jest.advanceTimersByTime(149); - expect(cb).not.toHaveBeenCalled(); - unmount(); - jest.advanceTimersByTime(100); - expect(cb).not.toHaveBeenCalled(); - }); - - it('should force execute callback after maxWait milliseconds', () => { - const cb = jest.fn(); - - const { result } = renderHook(() => useDebouncedCallback(cb, [], 150, 200)); - - result.current(); - expect(cb).not.toHaveBeenCalled(); - jest.advanceTimersByTime(149); - result.current(); - expect(cb).not.toHaveBeenCalled(); - jest.advanceTimersByTime(50); - expect(cb).not.toHaveBeenCalled(); - jest.advanceTimersByTime(1); - expect(cb).toHaveBeenCalledTimes(1); - }); - - it('should not execute callback twice if maxWait equals delay', () => { - const cb = jest.fn(); - - const { result } = renderHook(() => useDebouncedCallback(cb, [], 200, 200)); - - result.current(); - expect(cb).not.toHaveBeenCalled(); - jest.advanceTimersByTime(200); - expect(cb).toHaveBeenCalledTimes(1); - }); + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should be defined', () => { + expect(useDebouncedCallback).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => { + useDebouncedCallback(() => {}, [], 200); + }); + expect(result.error).toBeUndefined(); + }); + + it('should return function same length and wrapped name', () => { + let { result } = renderHook(() => + useDebouncedCallback((_a: any, _b: any, _c: any) => {}, [], 200) + ); + + expect(result.current.length).toBe(3); + expect(result.current.name).toBe(`anonymous__debounced__200`); + + function testFn(_a: any, _b: any, _c: any) {} + + result = renderHook(() => useDebouncedCallback(testFn, [], 100)).result; + + expect(result.current.length).toBe(3); + expect(result.current.name).toBe(`testFn__debounced__100`); + }); + + it('should return new callback if delay is changed', () => { + const { result, rerender } = renderHook( + ({ delay }) => useDebouncedCallback(() => {}, [], delay), + { + initialProps: { delay: 200 }, + } + ); + + const cb1 = result.current; + rerender({ delay: 123 }); + + expect(cb1).not.toBe(result.current); + }); + + it('should run given callback only after specified delay since last call', () => { + const cb = jest.fn(); + const { result } = renderHook(() => useDebouncedCallback(cb, [], 200)); + + result.current(); + expect(cb).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(100); + result.current(); + + jest.advanceTimersByTime(199); + expect(cb).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1); + expect(cb).toHaveBeenCalledTimes(1); + }); + + it('should pass parameters to callback', () => { + const cb = jest.fn((_a: number, _c: string) => {}); + const { result } = renderHook(() => useDebouncedCallback(cb, [], 200)); + + result.current(1, 'abc'); + jest.advanceTimersByTime(200); + expect(cb).toHaveBeenCalledWith(1, 'abc'); + }); + + it('should cancel previously scheduled call even if parameters changed', () => { + const cb1 = jest.fn(() => {}); + const cb2 = jest.fn(() => {}); + + const { result, rerender } = renderHook( + ({ i }) => + useDebouncedCallback( + () => { + if (i === 1) { + cb1(); + } else { + cb2(); + } + }, + [i], + 200 + ), + { initialProps: { i: 1 } } + ); + + result.current(); + jest.advanceTimersByTime(100); + + rerender({ i: 2 }); + result.current(); + jest.advanceTimersByTime(200); + + expect(cb1).not.toHaveBeenCalled(); + expect(cb2).toHaveBeenCalledTimes(1); + }); + + it('should cancel debounce execution after component unmount', () => { + const cb = jest.fn(); + + const { result, unmount } = renderHook(() => useDebouncedCallback(cb, [], 150, 200)); + + result.current(); + expect(cb).not.toHaveBeenCalled(); + jest.advanceTimersByTime(149); + expect(cb).not.toHaveBeenCalled(); + unmount(); + jest.advanceTimersByTime(100); + expect(cb).not.toHaveBeenCalled(); + }); + + it('should force execute callback after maxWait milliseconds', () => { + const cb = jest.fn(); + + const { result } = renderHook(() => useDebouncedCallback(cb, [], 150, 200)); + + result.current(); + expect(cb).not.toHaveBeenCalled(); + jest.advanceTimersByTime(149); + result.current(); + expect(cb).not.toHaveBeenCalled(); + jest.advanceTimersByTime(50); + expect(cb).not.toHaveBeenCalled(); + jest.advanceTimersByTime(1); + expect(cb).toHaveBeenCalledTimes(1); + }); + + it('should not execute callback twice if maxWait equals delay', () => { + const cb = jest.fn(); + + const { result } = renderHook(() => useDebouncedCallback(cb, [], 200, 200)); + + result.current(); + expect(cb).not.toHaveBeenCalled(); + jest.advanceTimersByTime(200); + expect(cb).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/useDebouncedCallback/__tests__/ssr.ts b/src/useDebouncedCallback/__tests__/ssr.ts index 6f77b2c24..b5075b25d 100644 --- a/src/useDebouncedCallback/__tests__/ssr.ts +++ b/src/useDebouncedCallback/__tests__/ssr.ts @@ -2,52 +2,52 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useDebouncedCallback } from '../..'; describe('useDebouncedCallback', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.clearAllTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - it('should be defined', () => { - expect(useDebouncedCallback).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => { - useDebouncedCallback(() => {}, [], 200); - }); - expect(result.error).toBeUndefined(); - }); - - it('should run given callback only after specified delay since last call', () => { - const cb = jest.fn(); - const { result } = renderHook(() => useDebouncedCallback(cb, [], 200)); - - result.current(); - expect(cb).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(100); - result.current(); - - jest.advanceTimersByTime(199); - expect(cb).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(1); - expect(cb).toHaveBeenCalledTimes(1); - }); - - it('should pass parameters to callback', () => { - const cb = jest.fn((_a: number, _c: string) => {}); - const { result } = renderHook(() => useDebouncedCallback(cb, [], 200)); - - result.current(1, 'abc'); - jest.advanceTimersByTime(200); - expect(cb).toHaveBeenCalledWith(1, 'abc'); - }); + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should be defined', () => { + expect(useDebouncedCallback).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => { + useDebouncedCallback(() => {}, [], 200); + }); + expect(result.error).toBeUndefined(); + }); + + it('should run given callback only after specified delay since last call', () => { + const cb = jest.fn(); + const { result } = renderHook(() => useDebouncedCallback(cb, [], 200)); + + result.current(); + expect(cb).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(100); + result.current(); + + jest.advanceTimersByTime(199); + expect(cb).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1); + expect(cb).toHaveBeenCalledTimes(1); + }); + + it('should pass parameters to callback', () => { + const cb = jest.fn((_a: number, _c: string) => {}); + const { result } = renderHook(() => useDebouncedCallback(cb, [], 200)); + + result.current(1, 'abc'); + jest.advanceTimersByTime(200); + expect(cb).toHaveBeenCalledWith(1, 'abc'); + }); }); diff --git a/src/useDebouncedCallback/index.ts b/src/useDebouncedCallback/index.ts index a13ce554e..f7996d79c 100644 --- a/src/useDebouncedCallback/index.ts +++ b/src/useDebouncedCallback/index.ts @@ -2,8 +2,8 @@ import { type DependencyList, useMemo, useRef } from 'react'; import { useUnmountEffect } from '../useUnmountEffect'; export type DebouncedFunction any> = ( - this: ThisParameterType, - ...args: Parameters + this: ThisParameterType, + ...args: Parameters ) => void; /** @@ -16,65 +16,65 @@ export type DebouncedFunction any> = ( * it's invoked. 0 means no max wait. */ export function useDebouncedCallback any>( - callback: Fn, - deps: DependencyList, - delay: number, - maxWait = 0 + callback: Fn, + deps: DependencyList, + delay: number, + maxWait = 0 ): DebouncedFunction { - const timeout = useRef>(); - const waitTimeout = useRef>(); - const lastCall = useRef<{ args: Parameters; this: ThisParameterType }>(); + const timeout = useRef>(); + const waitTimeout = useRef>(); + const lastCall = useRef<{ args: Parameters; this: ThisParameterType }>(); - const clear = () => { - if (timeout.current) { - clearTimeout(timeout.current); - timeout.current = undefined; - } + const clear = () => { + if (timeout.current) { + clearTimeout(timeout.current); + timeout.current = undefined; + } - if (waitTimeout.current) { - clearTimeout(waitTimeout.current); - waitTimeout.current = undefined; - } - }; + if (waitTimeout.current) { + clearTimeout(waitTimeout.current); + waitTimeout.current = undefined; + } + }; - // Cancel scheduled execution on unmount - useUnmountEffect(clear); + // Cancel scheduled execution on unmount + useUnmountEffect(clear); - return useMemo(() => { - const execute = () => { - // Barely possible to test this line - /* istanbul ignore next */ - if (!lastCall.current) return; + return useMemo(() => { + const execute = () => { + // Barely possible to test this line + /* istanbul ignore next */ + if (!lastCall.current) return; - const context = lastCall.current; - lastCall.current = undefined; + const context = lastCall.current; + lastCall.current = undefined; - callback.apply(context.this, context.args); + callback.apply(context.this, context.args); - clear(); - }; + clear(); + }; - const wrapped = function (this, ...args) { - if (timeout.current) { - clearTimeout(timeout.current); - } + const wrapped = function (this, ...args) { + if (timeout.current) { + clearTimeout(timeout.current); + } - lastCall.current = { args, this: this }; + lastCall.current = { args, this: this }; - // Plan regular execution - timeout.current = setTimeout(execute, delay); + // Plan regular execution + timeout.current = setTimeout(execute, delay); - // Plan maxWait execution if required - if (maxWait > 0 && !waitTimeout.current) { - waitTimeout.current = setTimeout(execute, maxWait); - } - } as DebouncedFunction; + // Plan maxWait execution if required + if (maxWait > 0 && !waitTimeout.current) { + waitTimeout.current = setTimeout(execute, maxWait); + } + } as DebouncedFunction; - Object.defineProperties(wrapped, { - length: { value: callback.length }, - name: { value: `${callback.name || 'anonymous'}__debounced__${delay}` }, - }); + Object.defineProperties(wrapped, { + length: { value: callback.length }, + name: { value: `${callback.name || 'anonymous'}__debounced__${delay}` }, + }); - return wrapped; - }, [delay, maxWait, ...deps]); + return wrapped; + }, [delay, maxWait, ...deps]); } diff --git a/src/useDebouncedEffect/__docs__/example.stories.tsx b/src/useDebouncedEffect/__docs__/example.stories.tsx index a4be6c471..de8b2b8e3 100644 --- a/src/useDebouncedEffect/__docs__/example.stories.tsx +++ b/src/useDebouncedEffect/__docs__/example.stories.tsx @@ -5,32 +5,32 @@ import { useDebouncedEffect } from '../..'; const HAS_DIGIT_REGEX = /\d/g; export function Example() { - const [state, setState] = useState(''); - const [hasNumbers, setHasNumbers] = useState(false); + const [state, setState] = useState(''); + const [hasNumbers, setHasNumbers] = useState(false); - useDebouncedEffect( - () => { - setHasNumbers(HAS_DIGIT_REGEX.test(state)); - }, - [state], - 200, - 500 - ); + useDebouncedEffect( + () => { + setHasNumbers(HAS_DIGIT_REGEX.test(state)); + }, + [state], + 200, + 500 + ); - return ( -
-
- Digit check will be performed 200ms after last change, but at least once every 500ms -
-
-
{hasNumbers ? 'Input has digits' : 'No digits found in input'}
- { - setState(ev.target.value); - }} - /> -
- ); + return ( +
+
+ Digit check will be performed 200ms after last change, but at least once every 500ms +
+
+
{hasNumbers ? 'Input has digits' : 'No digits found in input'}
+ { + setState(ev.target.value); + }} + /> +
+ ); } diff --git a/src/useDebouncedEffect/__docs__/story.mdx b/src/useDebouncedEffect/__docs__/story.mdx index f892daf5f..76d424685 100644 --- a/src/useDebouncedEffect/__docs__/story.mdx +++ b/src/useDebouncedEffect/__docs__/story.mdx @@ -11,17 +11,17 @@ Like `useEffect`, but the passed function is debounced. #### Example - + ## Reference ```ts export function useDebouncedEffect( - callback: (...args: any[]) => void, - deps: DependencyList, - delay: number, - maxWait = 0 + callback: (...args: any[]) => void, + deps: DependencyList, + delay: number, + maxWait = 0 ): void; ``` diff --git a/src/useDebouncedEffect/__tests__/dom.ts b/src/useDebouncedEffect/__tests__/dom.ts index 324a0a62c..8ffe597f1 100644 --- a/src/useDebouncedEffect/__tests__/dom.ts +++ b/src/useDebouncedEffect/__tests__/dom.ts @@ -2,41 +2,41 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useDebouncedEffect } from '../..'; describe('useDebouncedEffect', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.clearAllTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - it('should be defined', () => { - expect(useDebouncedEffect).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => { - useDebouncedEffect(() => {}, [], 200); - }); - expect(result.error).toBeUndefined(); - }); - - it('should call effect only after delay', () => { - const spy = jest.fn(); - - renderHook(() => { - useDebouncedEffect(spy, [], 200); - }); - expect(spy).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(199); - expect(spy).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(1); - expect(spy).toHaveBeenCalledTimes(1); - }); + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should be defined', () => { + expect(useDebouncedEffect).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => { + useDebouncedEffect(() => {}, [], 200); + }); + expect(result.error).toBeUndefined(); + }); + + it('should call effect only after delay', () => { + const spy = jest.fn(); + + renderHook(() => { + useDebouncedEffect(spy, [], 200); + }); + expect(spy).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(199); + expect(spy).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1); + expect(spy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/useDebouncedEffect/__tests__/ssr.ts b/src/useDebouncedEffect/__tests__/ssr.ts index f64b7a2ca..03bbaaee6 100644 --- a/src/useDebouncedEffect/__tests__/ssr.ts +++ b/src/useDebouncedEffect/__tests__/ssr.ts @@ -2,26 +2,26 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useDebouncedEffect } from '../..'; describe('useDebouncedEffect', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); + beforeAll(() => { + jest.useFakeTimers(); + }); - afterEach(() => { - jest.clearAllTimers(); - }); + afterEach(() => { + jest.clearAllTimers(); + }); - afterAll(() => { - jest.useRealTimers(); - }); + afterAll(() => { + jest.useRealTimers(); + }); - it('should be defined', () => { - expect(useDebouncedEffect).toBeDefined(); - }); + it('should be defined', () => { + expect(useDebouncedEffect).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useDebouncedEffect(() => {}, [], 200); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useDebouncedEffect(() => {}, [], 200); + }); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useDebouncedEffect/index.ts b/src/useDebouncedEffect/index.ts index 0aff71383..5037a3c86 100644 --- a/src/useDebouncedEffect/index.ts +++ b/src/useDebouncedEffect/index.ts @@ -12,10 +12,10 @@ import { useDebouncedCallback } from '../useDebouncedCallback'; * before it's invoked. `0` means no max wait. */ export function useDebouncedEffect( - callback: (...args: any[]) => void, - deps: DependencyList, - delay: number, - maxWait = 0 + callback: (...args: any[]) => void, + deps: DependencyList, + delay: number, + maxWait = 0 ): void { - useEffect(useDebouncedCallback(callback, deps, delay, maxWait), deps); + useEffect(useDebouncedCallback(callback, deps, delay, maxWait), deps); } diff --git a/src/useDebouncedState/__docs__/example.stories.tsx b/src/useDebouncedState/__docs__/example.stories.tsx index c6c02494d..48b3040b4 100644 --- a/src/useDebouncedState/__docs__/example.stories.tsx +++ b/src/useDebouncedState/__docs__/example.stories.tsx @@ -2,19 +2,19 @@ import * as React from 'react'; import { useDebouncedState } from '../..'; export function Example() { - const [state, setState] = useDebouncedState('', 300, 500); + const [state, setState] = useDebouncedState('', 300, 500); - return ( -
-
Below state will update 300ms after last change, but at least once every 500ms
-
-
The input`s value is: {state}
- { - setState(ev.target.value); - }} - /> -
- ); + return ( +
+
Below state will update 300ms after last change, but at least once every 500ms
+
+
The input`s value is: {state}
+ { + setState(ev.target.value); + }} + /> +
+ ); } diff --git a/src/useDebouncedState/__docs__/story.mdx b/src/useDebouncedState/__docs__/story.mdx index 005d095ce..57933dbe7 100644 --- a/src/useDebouncedState/__docs__/story.mdx +++ b/src/useDebouncedState/__docs__/story.mdx @@ -11,16 +11,16 @@ Like `useState` but its state setter is debounced. #### Example - + ## Reference ```ts export function useDebouncedState( - initialState: S | (() => S), - delay: number, - maxWait = 0 + initialState: S | (() => S), + delay: number, + maxWait = 0 ): [S, Dispatch>]; ``` diff --git a/src/useDebouncedState/__tests__/dom.ts b/src/useDebouncedState/__tests__/dom.ts index bbeec1dfb..79397d5ca 100644 --- a/src/useDebouncedState/__tests__/dom.ts +++ b/src/useDebouncedState/__tests__/dom.ts @@ -2,41 +2,41 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { useDebouncedState } from '../..'; describe('useDebouncedState', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.clearAllTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - it('should be defined', () => { - expect(useDebouncedState).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useDebouncedState(undefined, 200)); - expect(result.error).toBeUndefined(); - }); - - it('should debounce state set', () => { - const { result } = renderHook(() => useDebouncedState(undefined, 200)); - - expect(result.current[0]).toBe(undefined); - result.current[1]('Hello world!'); - - act(() => { - jest.advanceTimersByTime(199); - }); - expect(result.current[0]).toBe(undefined); - - act(() => { - jest.advanceTimersByTime(1); - }); - expect(result.current[0]).toBe('Hello world!'); - }); + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should be defined', () => { + expect(useDebouncedState).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useDebouncedState(undefined, 200)); + expect(result.error).toBeUndefined(); + }); + + it('should debounce state set', () => { + const { result } = renderHook(() => useDebouncedState(undefined, 200)); + + expect(result.current[0]).toBe(undefined); + result.current[1]('Hello world!'); + + act(() => { + jest.advanceTimersByTime(199); + }); + expect(result.current[0]).toBe(undefined); + + act(() => { + jest.advanceTimersByTime(1); + }); + expect(result.current[0]).toBe('Hello world!'); + }); }); diff --git a/src/useDebouncedState/__tests__/ssr.ts b/src/useDebouncedState/__tests__/ssr.ts index 2635cc7b2..49c6ec074 100644 --- a/src/useDebouncedState/__tests__/ssr.ts +++ b/src/useDebouncedState/__tests__/ssr.ts @@ -2,24 +2,24 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useDebouncedState } from '../..'; describe('useDebouncedState', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); + beforeAll(() => { + jest.useFakeTimers(); + }); - afterEach(() => { - jest.clearAllTimers(); - }); + afterEach(() => { + jest.clearAllTimers(); + }); - afterAll(() => { - jest.useRealTimers(); - }); + afterAll(() => { + jest.useRealTimers(); + }); - it('should be defined', () => { - expect(useDebouncedState).toBeDefined(); - }); + it('should be defined', () => { + expect(useDebouncedState).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useDebouncedState(undefined, 200)); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useDebouncedState(undefined, 200)); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useDebouncedState/index.ts b/src/useDebouncedState/index.ts index 8ae4629bb..156ad556e 100644 --- a/src/useDebouncedState/index.ts +++ b/src/useDebouncedState/index.ts @@ -10,11 +10,11 @@ import { useDebouncedCallback } from '../useDebouncedCallback'; * before it's force execution. 0 means no max wait. */ export function useDebouncedState( - initialState: S | (() => S), - delay: number, - maxWait = 0 + initialState: S | (() => S), + delay: number, + maxWait = 0 ): [S, Dispatch>] { - const [state, setState] = useState(initialState); + const [state, setState] = useState(initialState); - return [state, useDebouncedCallback(setState, [], delay, maxWait)]; + return [state, useDebouncedCallback(setState, [], delay, maxWait)]; } diff --git a/src/useDeepCompareEffect/__docs__/example.stories.tsx b/src/useDeepCompareEffect/__docs__/example.stories.tsx index 091e39064..47f012316 100644 --- a/src/useDeepCompareEffect/__docs__/example.stories.tsx +++ b/src/useDeepCompareEffect/__docs__/example.stories.tsx @@ -3,32 +3,32 @@ import { useEffect } from 'react'; import { useDeepCompareEffect, useRerender } from '../..'; export function Example() { - const rerender = useRerender(); + const rerender = useRerender(); - // eslint-disable-next-line react-hooks/exhaustive-deps - const newOnEveryRender = { - name: 'Foo', - }; + // eslint-disable-next-line react-hooks/exhaustive-deps + const newOnEveryRender = { + name: 'Foo', + }; - useEffect(() => { - console.log('I do get logged on every render.'); - }, [newOnEveryRender]); + useEffect(() => { + console.log('I do get logged on every render.'); + }, [newOnEveryRender]); - useDeepCompareEffect(() => { - console.log('I do not get logged on every render.'); - }, [newOnEveryRender]); + useDeepCompareEffect(() => { + console.log('I do not get logged on every render.'); + }, [newOnEveryRender]); - return ( - <> -

Open you browser console and the code for this example.

-

- Repeatedly press the button below. Notice, how the useEffect gets run on every render, but - useDeepCompareEffect does not. This is because useDeepCompareEffect determines dependency - changes by deep comparison instead of by reference like useEffect. -

- - - ); + return ( + <> +

Open you browser console and the code for this example.

+

+ Repeatedly press the button below. Notice, how the useEffect gets run on every render, but + useDeepCompareEffect does not. This is because useDeepCompareEffect determines dependency + changes by deep comparison instead of by reference like useEffect. +

+ + + ); } diff --git a/src/useDeepCompareEffect/__docs__/story.mdx b/src/useDeepCompareEffect/__docs__/story.mdx index 349a0f1ee..0f5d3659f 100644 --- a/src/useDeepCompareEffect/__docs__/story.mdx +++ b/src/useDeepCompareEffect/__docs__/story.mdx @@ -16,22 +16,22 @@ changes. #### Example - + ## Reference ```ts export function useCustomCompareEffect< - Callback extends EffectCallback = EffectCallback, - Deps extends DependencyList = DependencyList, - HookRestArgs extends any[] = any[], - R extends HookRestArgs = HookRestArgs + Callback extends EffectCallback = EffectCallback, + Deps extends DependencyList = DependencyList, + HookRestArgs extends any[] = any[], + R extends HookRestArgs = HookRestArgs >( - callback: Callback, - deps: Deps, - effectHook: EffectHook = useEffect, - ...effectHookRestArgs: R + callback: Callback, + deps: Deps, + effectHook: EffectHook = useEffect, + ...effectHookRestArgs: R ): void; ``` diff --git a/src/useDeepCompareEffect/__tests__/dom.ts b/src/useDeepCompareEffect/__tests__/dom.ts index 6bc02877b..50e68dd71 100644 --- a/src/useDeepCompareEffect/__tests__/dom.ts +++ b/src/useDeepCompareEffect/__tests__/dom.ts @@ -2,37 +2,37 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useDeepCompareEffect } from '../..'; describe('useDeepCompareEffect', () => { - it('should be defined', () => { - expect(useDeepCompareEffect).toBeDefined(); - }); + it('should be defined', () => { + expect(useDeepCompareEffect).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useDeepCompareEffect(() => {}, []); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useDeepCompareEffect(() => {}, []); + }); + expect(result.error).toBeUndefined(); + }); - it('should run only in case deps are changed', () => { - const spy = jest.fn(); - const { rerender } = renderHook( - ({ deps }) => { - useDeepCompareEffect(spy, deps); - }, - { - initialProps: { deps: [{ foo: 'bar' }] }, - } - ); + it('should run only in case deps are changed', () => { + const spy = jest.fn(); + const { rerender } = renderHook( + ({ deps }) => { + useDeepCompareEffect(spy, deps); + }, + { + initialProps: { deps: [{ foo: 'bar' }] }, + } + ); - expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledTimes(1); - rerender({ deps: [{ foo: 'bar' }] }); - expect(spy).toHaveBeenCalledTimes(1); + rerender({ deps: [{ foo: 'bar' }] }); + expect(spy).toHaveBeenCalledTimes(1); - rerender({ deps: [{ foo: 'baz' }] }); - expect(spy).toHaveBeenCalledTimes(2); + rerender({ deps: [{ foo: 'baz' }] }); + expect(spy).toHaveBeenCalledTimes(2); - rerender({ deps: [{ foo: 'baz' }] }); - expect(spy).toHaveBeenCalledTimes(2); - }); + rerender({ deps: [{ foo: 'baz' }] }); + expect(spy).toHaveBeenCalledTimes(2); + }); }); diff --git a/src/useDeepCompareEffect/__tests__/ssr.ts b/src/useDeepCompareEffect/__tests__/ssr.ts index 554ee5ddc..aaaa78c12 100644 --- a/src/useDeepCompareEffect/__tests__/ssr.ts +++ b/src/useDeepCompareEffect/__tests__/ssr.ts @@ -2,14 +2,14 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useDeepCompareEffect } from '../..'; describe('useDeepCompareEffect', () => { - it('should be defined', () => { - expect(useDeepCompareEffect).toBeDefined(); - }); + it('should be defined', () => { + expect(useDeepCompareEffect).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useDeepCompareEffect(() => {}, []); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useDeepCompareEffect(() => {}, []); + }); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useDeepCompareEffect/index.ts b/src/useDeepCompareEffect/index.ts index cce9ed7c3..d5037af58 100644 --- a/src/useDeepCompareEffect/index.ts +++ b/src/useDeepCompareEffect/index.ts @@ -16,15 +16,15 @@ import { type EffectCallback, type EffectHook } from '../util/misc'; * after the `callback` and the dependency list. */ export function useDeepCompareEffect< - Callback extends EffectCallback = EffectCallback, - Deps extends DependencyList = DependencyList, - HookRestArgs extends any[] = any[], - R extends HookRestArgs = HookRestArgs + Callback extends EffectCallback = EffectCallback, + Deps extends DependencyList = DependencyList, + HookRestArgs extends any[] = any[], + R extends HookRestArgs = HookRestArgs >( - callback: Callback, - deps: Deps, - effectHook: EffectHook = useEffect, - ...effectHookRestArgs: R + callback: Callback, + deps: Deps, + effectHook: EffectHook = useEffect, + ...effectHookRestArgs: R ): void { - useCustomCompareEffect(callback, deps, isEqual, effectHook, ...effectHookRestArgs); + useCustomCompareEffect(callback, deps, isEqual, effectHook, ...effectHookRestArgs); } diff --git a/src/useDeepCompareMemo/__docs__/example.stories.tsx b/src/useDeepCompareMemo/__docs__/example.stories.tsx index 2262eb55c..e929496d3 100644 --- a/src/useDeepCompareMemo/__docs__/example.stories.tsx +++ b/src/useDeepCompareMemo/__docs__/example.stories.tsx @@ -3,26 +3,26 @@ import { useMemo } from 'react'; import { useRerender, useDeepCompareMemo } from '../..'; export function Example() { - const newOnEveryRender = { value: 'Foo' }; - // eslint-disable-next-line react-hooks/exhaustive-deps - const unstable = useMemo(() => Math.floor(Math.random() * 10), [newOnEveryRender]); + const newOnEveryRender = { value: 'Foo' }; + // eslint-disable-next-line react-hooks/exhaustive-deps + const unstable = useMemo(() => Math.floor(Math.random() * 10), [newOnEveryRender]); - const stable = useDeepCompareMemo(() => Math.floor(Math.random() * 10), [newOnEveryRender]); + const stable = useDeepCompareMemo(() => Math.floor(Math.random() * 10), [newOnEveryRender]); - const rerender = useRerender(); - return ( - <> -
-

When you click this button:

- -

, you notice, that the useDeepCompareMemo value does not change at all,

-
-

even though its dependencies change on every render.

-
-

useMemo: {unstable}

-

useDeepCompareMemo: {stable}

- - ); + const rerender = useRerender(); + return ( + <> +
+

When you click this button:

+ +

, you notice, that the useDeepCompareMemo value does not change at all,

+
+

even though its dependencies change on every render.

+
+

useMemo: {unstable}

+

useDeepCompareMemo: {stable}

+ + ); } diff --git a/src/useDeepCompareMemo/__docs__/story.mdx b/src/useDeepCompareMemo/__docs__/story.mdx index ba988fcac..7eaf075b8 100644 --- a/src/useDeepCompareMemo/__docs__/story.mdx +++ b/src/useDeepCompareMemo/__docs__/story.mdx @@ -14,7 +14,7 @@ Like `useMemo` but uses `@react-hookz/deep-equal` comparator function to validat #### Example - + ## Reference diff --git a/src/useDeepCompareMemo/__tests__/dom.ts b/src/useDeepCompareMemo/__tests__/dom.ts index 5181f9b2c..77fbedaa6 100644 --- a/src/useDeepCompareMemo/__tests__/dom.ts +++ b/src/useDeepCompareMemo/__tests__/dom.ts @@ -2,32 +2,32 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useDeepCompareMemo } from '../..'; describe('useDeepCompareMemo', () => { - it('should be defined', () => { - expect(useDeepCompareMemo).toBeDefined(); - }); + it('should be defined', () => { + expect(useDeepCompareMemo).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useDeepCompareMemo(() => {}, []); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useDeepCompareMemo(() => {}, []); + }); + expect(result.error).toBeUndefined(); + }); - it('should run only if dependencies change, defined by deep comparison', () => { - const spy = jest.fn(); - const { rerender } = renderHook(({ deps }) => useDeepCompareMemo(spy, deps), { - initialProps: { deps: [{ foo: 'bar' }] }, - }); + it('should run only if dependencies change, defined by deep comparison', () => { + const spy = jest.fn(); + const { rerender } = renderHook(({ deps }) => useDeepCompareMemo(spy, deps), { + initialProps: { deps: [{ foo: 'bar' }] }, + }); - expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledTimes(1); - rerender({ deps: [{ foo: 'bar' }] }); - expect(spy).toHaveBeenCalledTimes(1); + rerender({ deps: [{ foo: 'bar' }] }); + expect(spy).toHaveBeenCalledTimes(1); - rerender({ deps: [{ foo: 'baz' }] }); - expect(spy).toHaveBeenCalledTimes(2); + rerender({ deps: [{ foo: 'baz' }] }); + expect(spy).toHaveBeenCalledTimes(2); - rerender({ deps: [{ foo: 'baz' }] }); - expect(spy).toHaveBeenCalledTimes(2); - }); + rerender({ deps: [{ foo: 'baz' }] }); + expect(spy).toHaveBeenCalledTimes(2); + }); }); diff --git a/src/useDeepCompareMemo/__tests__/ssr.ts b/src/useDeepCompareMemo/__tests__/ssr.ts index af5b99b9e..79a1b7194 100644 --- a/src/useDeepCompareMemo/__tests__/ssr.ts +++ b/src/useDeepCompareMemo/__tests__/ssr.ts @@ -2,14 +2,14 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useDeepCompareMemo } from '../..'; describe('useDeepCompareMemo', () => { - it('should be defined', () => { - expect(useDeepCompareMemo).toBeDefined(); - }); + it('should be defined', () => { + expect(useDeepCompareMemo).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useDeepCompareMemo(() => {}, []); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useDeepCompareMemo(() => {}, []); + }); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useDeepCompareMemo/index.ts b/src/useDeepCompareMemo/index.ts index 248711ab7..d45cca720 100644 --- a/src/useDeepCompareMemo/index.ts +++ b/src/useDeepCompareMemo/index.ts @@ -11,5 +11,5 @@ import { useCustomCompareMemo } from '../useCustomCompareMemo'; * the same value, if dependencies haven't changed, or the result of calling `factory` again, if they have changed. */ export function useDeepCompareMemo(factory: () => T, deps: Deps) { - return useCustomCompareMemo(factory, deps, isEqual); + return useCustomCompareMemo(factory, deps, isEqual); } diff --git a/src/useDocumentVisibility/__docs__/example.stories.tsx b/src/useDocumentVisibility/__docs__/example.stories.tsx index f24529b37..fc67ab89b 100644 --- a/src/useDocumentVisibility/__docs__/example.stories.tsx +++ b/src/useDocumentVisibility/__docs__/example.stories.tsx @@ -3,20 +3,20 @@ import { useEffect } from 'react'; import { useDocumentVisibility } from '../..'; export function Example() { - const isVisible = useDocumentVisibility(); + const isVisible = useDocumentVisibility(); - useEffect(() => { - if (!isVisible) { - // eslint-disable-next-line no-alert - alert('Document was not visible'); - } - }, [isVisible]); + useEffect(() => { + if (!isVisible) { + // eslint-disable-next-line no-alert + alert('Document was not visible'); + } + }, [isVisible]); - return ( -
-

- The document is {isVisible ? 'visible' : 'hidden'} -

-
- ); + return ( +
+

+ The document is {isVisible ? 'visible' : 'hidden'} +

+
+ ); } diff --git a/src/useDocumentVisibility/__docs__/story.mdx b/src/useDocumentVisibility/__docs__/story.mdx index 0d70a50e2..46ed8df36 100644 --- a/src/useDocumentVisibility/__docs__/story.mdx +++ b/src/useDocumentVisibility/__docs__/story.mdx @@ -11,7 +11,7 @@ Returns a boolean indicating whether the document is visible or not. #### Example - + ## Reference diff --git a/src/useDocumentVisibility/__tests__/dom.ts b/src/useDocumentVisibility/__tests__/dom.ts index 35d7a4499..7c41286da 100644 --- a/src/useDocumentVisibility/__tests__/dom.ts +++ b/src/useDocumentVisibility/__tests__/dom.ts @@ -2,69 +2,69 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { useDocumentVisibility } from '../..'; describe('useDocumentVisibility', () => { - it('should be defined', () => { - expect(useDocumentVisibility).toBeDefined(); - }); + it('should be defined', () => { + expect(useDocumentVisibility).toBeDefined(); + }); - it('should return current visibility state if initializing with value', () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - value: 'hidden', - }); - expect(renderHook(() => useDocumentVisibility()).result.current).toBe(false); + it('should return current visibility state if initializing with value', () => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + value: 'hidden', + }); + expect(renderHook(() => useDocumentVisibility()).result.current).toBe(false); - Object.defineProperty(document, 'visibilityState', { - configurable: true, - value: 'visible', - }); - expect(renderHook(() => useDocumentVisibility(true)).result.current).toBe(true); - }); + Object.defineProperty(document, 'visibilityState', { + configurable: true, + value: 'visible', + }); + expect(renderHook(() => useDocumentVisibility(true)).result.current).toBe(true); + }); - it('should return undefined on first render and set state on effects stage if not initializing with value', () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - value: 'hidden', - }); + it('should return undefined on first render and set state on effects stage if not initializing with value', () => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + value: 'hidden', + }); - { - const { result } = renderHook(() => useDocumentVisibility(false)); + { + const { result } = renderHook(() => useDocumentVisibility(false)); - expect(result.current).toBe(false); - expect(result.all[0]).toBe(undefined); - } + expect(result.current).toBe(false); + expect(result.all[0]).toBe(undefined); + } - Object.defineProperty(document, 'visibilityState', { - configurable: true, - value: 'visible', - }); + Object.defineProperty(document, 'visibilityState', { + configurable: true, + value: 'visible', + }); - { - const { result } = renderHook(() => useDocumentVisibility(false)); + { + const { result } = renderHook(() => useDocumentVisibility(false)); - expect(result.current).toBe(true); - expect(result.all[0]).toBe(undefined); - } - }); + expect(result.current).toBe(true); + expect(result.all[0]).toBe(undefined); + } + }); - it('should update state on visibilitychange event', () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - value: 'hidden', - }); + it('should update state on visibilitychange event', () => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + value: 'hidden', + }); - const { result } = renderHook(() => useDocumentVisibility()); + const { result } = renderHook(() => useDocumentVisibility()); - expect(result.current).toBe(false); + expect(result.current).toBe(false); - Object.defineProperty(document, 'visibilityState', { - configurable: true, - value: 'visible', - }); + Object.defineProperty(document, 'visibilityState', { + configurable: true, + value: 'visible', + }); - act(() => { - document.dispatchEvent(new Event('visibilitychange')); - }); + act(() => { + document.dispatchEvent(new Event('visibilitychange')); + }); - expect(result.current).toBe(true); - }); + expect(result.current).toBe(true); + }); }); diff --git a/src/useDocumentVisibility/__tests__/ssr.ts b/src/useDocumentVisibility/__tests__/ssr.ts index ba7498ae9..a6faf5db7 100644 --- a/src/useDocumentVisibility/__tests__/ssr.ts +++ b/src/useDocumentVisibility/__tests__/ssr.ts @@ -2,13 +2,13 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useDocumentVisibility } from '../..'; describe('useDocumentVisibility', () => { - it('should be defined', () => { - expect(useDocumentVisibility).toBeDefined(); - }); + it('should be defined', () => { + expect(useDocumentVisibility).toBeDefined(); + }); - it('should return undefined regardless of `initializeWithValue` parameter', () => { - expect(renderHook(() => useDocumentVisibility()).result.current).toBeUndefined(); - expect(renderHook(() => useDocumentVisibility(true)).result.current).toBeUndefined(); - expect(renderHook(() => useDocumentVisibility(false)).result.current).toBeUndefined(); - }); + it('should return undefined regardless of `initializeWithValue` parameter', () => { + expect(renderHook(() => useDocumentVisibility()).result.current).toBeUndefined(); + expect(renderHook(() => useDocumentVisibility(true)).result.current).toBeUndefined(); + expect(renderHook(() => useDocumentVisibility(false)).result.current).toBeUndefined(); + }); }); diff --git a/src/useDocumentVisibility/index.ts b/src/useDocumentVisibility/index.ts index 231430ea2..42ea2738c 100644 --- a/src/useDocumentVisibility/index.ts +++ b/src/useDocumentVisibility/index.ts @@ -14,19 +14,19 @@ export function useDocumentVisibility(initializeWithValue?: true): boolean; * `undefined`. _Set this to `false` during SSR._ */ export function useDocumentVisibility(initializeWithValue = true): boolean | undefined { - const [isVisible, setIsVisible] = useState( - isBrowser && initializeWithValue ? isDocumentVisible() : undefined - ); + const [isVisible, setIsVisible] = useState( + isBrowser && initializeWithValue ? isDocumentVisible() : undefined + ); - useMountEffect(() => { - if (!initializeWithValue) { - setIsVisible(isDocumentVisible()); - } - }); + useMountEffect(() => { + if (!initializeWithValue) { + setIsVisible(isDocumentVisible()); + } + }); - useEventListener(isBrowser ? document : null, 'visibilitychange', () => { - setIsVisible(isDocumentVisible()); - }); + useEventListener(isBrowser ? document : null, 'visibilitychange', () => { + setIsVisible(isDocumentVisible()); + }); - return isVisible; + return isVisible; } diff --git a/src/useEventListener/__docs__/example.stories.tsx b/src/useEventListener/__docs__/example.stories.tsx index d67a45500..1ec66bf54 100644 --- a/src/useEventListener/__docs__/example.stories.tsx +++ b/src/useEventListener/__docs__/example.stories.tsx @@ -3,45 +3,45 @@ import { useState } from 'react'; import { useEventListener, useToggle } from '../..'; export function Example() { - const [state, setState] = useState(); - const [mounted, toggleMounted] = useToggle(true); + const [state, setState] = useState(); + const [mounted, toggleMounted] = useToggle(true); - function ToggledComponent() { - useEventListener( - window, - 'mousemove', - () => { - setState(new Date()); - }, - { passive: true } - ); + function ToggledComponent() { + useEventListener( + window, + 'mousemove', + () => { + setState(new Date()); + }, + { passive: true } + ); - return
Datetime updating component is mounted.
; - } + return
Datetime updating component is mounted.
; + } - return ( -
-
- The datetime shown below is updated on window's mousemove event. -
- You can unmount the datetime updating component by clicking the button below to ensure that - the event is unsubscribed from when the component unmounts. -
+ return ( +
+
+ The datetime shown below is updated on window's mousemove event. +
+ You can unmount the datetime updating component by clicking the button below to ensure that + the event is unsubscribed from when the component unmounts. +
-
-
{state ? `Cursor last moved: ${state.toString()}.` : 'Cursor not moved yet.'}
+
+
{state ? `Cursor last moved: ${state.toString()}.` : 'Cursor not moved yet.'}
-
-
- {mounted && } - -
-
- ); +
+
+ {mounted && } + +
+
+ ); } diff --git a/src/useEventListener/__docs__/story.mdx b/src/useEventListener/__docs__/story.mdx index 743ddd275..8c9009b94 100644 --- a/src/useEventListener/__docs__/story.mdx +++ b/src/useEventListener/__docs__/story.mdx @@ -16,17 +16,17 @@ Subscribes an event listener to a target element. #### Example - + ## Reference ```ts export function useEventListener( - target: RefObject | T | null, - ...params: - | Parameters - | [string, EventListenerOrEventListenerObject | ((...args: any[]) => any), ...any] + target: RefObject | T | null, + ...params: + | Parameters + | [string, EventListenerOrEventListenerObject | ((...args: any[]) => any), ...any] ): void; ``` diff --git a/src/useEventListener/__tests__/dom.ts b/src/useEventListener/__tests__/dom.ts index c3c46b4fc..ba1ce4c99 100644 --- a/src/useEventListener/__tests__/dom.ts +++ b/src/useEventListener/__tests__/dom.ts @@ -2,96 +2,96 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useEventListener } from '../..'; describe('useEventListener', () => { - it('should be defined', () => { - expect(useEventListener).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => { - useEventListener(null, '', () => {}); - }); - expect(result.error).toBeUndefined(); - }); - - it('should bind listener on mount and unbind on unmount', () => { - const div = document.createElement('div'); - const addSpy = jest.spyOn(div, 'addEventListener'); - const removeSpy = jest.spyOn(div, 'removeEventListener'); - - const { rerender, unmount } = renderHook(() => { - useEventListener(div, 'resize', () => {}, { passive: true }); - }); - - expect(addSpy).toHaveBeenCalledTimes(1); - expect(removeSpy).toHaveBeenCalledTimes(0); - - rerender(); - expect(addSpy).toHaveBeenCalledTimes(1); - expect(removeSpy).toHaveBeenCalledTimes(0); - - unmount(); - expect(addSpy).toHaveBeenCalledTimes(1); - expect(removeSpy).toHaveBeenCalledTimes(1); - }); - - it('should work with react refs', () => { - const div = document.createElement('div'); - const addSpy = jest.spyOn(div, 'addEventListener'); - const removeSpy = jest.spyOn(div, 'removeEventListener'); - - const ref = { current: div }; - const { rerender, unmount } = renderHook(() => { - useEventListener(ref, 'resize', () => {}, { passive: true }); - }); - - expect(addSpy).toHaveBeenCalledTimes(1); - expect(addSpy.mock.calls[0][2]).toStrictEqual({ passive: true }); - expect(removeSpy).toHaveBeenCalledTimes(0); - - rerender(); - expect(addSpy).toHaveBeenCalledTimes(1); - expect(removeSpy).toHaveBeenCalledTimes(0); - - unmount(); - expect(addSpy).toHaveBeenCalledTimes(1); - expect(removeSpy).toHaveBeenCalledTimes(1); - }); - - it('should invoke provided function on event trigger with proper context', () => { - const div = document.createElement('div'); - let context: any; - const spy = jest.fn(function (this: any) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - context = this; - }); - - renderHook(() => { - useEventListener(div, 'resize', spy, { passive: true }); - }); - - const evt = new Event('resize'); - div.dispatchEvent(evt); - - expect(spy).toHaveBeenCalledWith(evt); - expect(context).toBe(div); - }); - - it('should properly handle event listener objects', () => { - const div = document.createElement('div'); - let context: any; - const spy = jest.fn(function (this: any) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - context = this; - }); - - renderHook(() => { - useEventListener(div, 'resize', { handleEvent: spy }, { passive: true }); - }); - - const evt = new Event('resize'); - div.dispatchEvent(evt); - - expect(spy).toHaveBeenCalledWith(evt); - expect(context).toBe(div); - }); + it('should be defined', () => { + expect(useEventListener).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => { + useEventListener(null, '', () => {}); + }); + expect(result.error).toBeUndefined(); + }); + + it('should bind listener on mount and unbind on unmount', () => { + const div = document.createElement('div'); + const addSpy = jest.spyOn(div, 'addEventListener'); + const removeSpy = jest.spyOn(div, 'removeEventListener'); + + const { rerender, unmount } = renderHook(() => { + useEventListener(div, 'resize', () => {}, { passive: true }); + }); + + expect(addSpy).toHaveBeenCalledTimes(1); + expect(removeSpy).toHaveBeenCalledTimes(0); + + rerender(); + expect(addSpy).toHaveBeenCalledTimes(1); + expect(removeSpy).toHaveBeenCalledTimes(0); + + unmount(); + expect(addSpy).toHaveBeenCalledTimes(1); + expect(removeSpy).toHaveBeenCalledTimes(1); + }); + + it('should work with react refs', () => { + const div = document.createElement('div'); + const addSpy = jest.spyOn(div, 'addEventListener'); + const removeSpy = jest.spyOn(div, 'removeEventListener'); + + const ref = { current: div }; + const { rerender, unmount } = renderHook(() => { + useEventListener(ref, 'resize', () => {}, { passive: true }); + }); + + expect(addSpy).toHaveBeenCalledTimes(1); + expect(addSpy.mock.calls[0][2]).toStrictEqual({ passive: true }); + expect(removeSpy).toHaveBeenCalledTimes(0); + + rerender(); + expect(addSpy).toHaveBeenCalledTimes(1); + expect(removeSpy).toHaveBeenCalledTimes(0); + + unmount(); + expect(addSpy).toHaveBeenCalledTimes(1); + expect(removeSpy).toHaveBeenCalledTimes(1); + }); + + it('should invoke provided function on event trigger with proper context', () => { + const div = document.createElement('div'); + let context: any; + const spy = jest.fn(function (this: any) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + context = this; + }); + + renderHook(() => { + useEventListener(div, 'resize', spy, { passive: true }); + }); + + const evt = new Event('resize'); + div.dispatchEvent(evt); + + expect(spy).toHaveBeenCalledWith(evt); + expect(context).toBe(div); + }); + + it('should properly handle event listener objects', () => { + const div = document.createElement('div'); + let context: any; + const spy = jest.fn(function (this: any) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + context = this; + }); + + renderHook(() => { + useEventListener(div, 'resize', { handleEvent: spy }, { passive: true }); + }); + + const evt = new Event('resize'); + div.dispatchEvent(evt); + + expect(spy).toHaveBeenCalledWith(evt); + expect(context).toBe(div); + }); }); diff --git a/src/useEventListener/__tests__/ssr.ts b/src/useEventListener/__tests__/ssr.ts index 64b410be6..f6996f655 100644 --- a/src/useEventListener/__tests__/ssr.ts +++ b/src/useEventListener/__tests__/ssr.ts @@ -2,14 +2,14 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useEventListener } from '../..'; describe('useEventListener', () => { - it('should be defined', () => { - expect(useEventListener).toBeDefined(); - }); + it('should be defined', () => { + expect(useEventListener).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useEventListener(null, 'random name', () => {}); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useEventListener(null, 'random name', () => {}); + }); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useEventListener/index.ts b/src/useEventListener/index.ts index a1b887469..8a7aa10d7 100644 --- a/src/useEventListener/index.ts +++ b/src/useEventListener/index.ts @@ -11,50 +11,50 @@ import { hasOwnProperty, off, on } from '../util/misc'; * something like `[eventName, listener, options]`. */ export function useEventListener( - target: RefObject | T | null, - ...params: - | Parameters - | [string, EventListenerOrEventListenerObject | ((...args: any[]) => any), ...any] + target: RefObject | T | null, + ...params: + | Parameters + | [string, EventListenerOrEventListenerObject | ((...args: any[]) => any), ...any] ): void { - const isMounted = useIsMounted(); - - // Create static event listener - const listenerRef = useSyncedRef(params[1]); - const eventListener = useMemo( - () => - // As some event listeners designed to be used through `this` - // it is better to make listener a conventional function as it - // infers call context - - function (this: T, ...args) { - // Normally, such situation should not happen, but better to - // have back covered - /* istanbul ignore next */ - if (!isMounted()) return; - - // We dont care if non-listener provided, simply dont do anything - /* istanbul ignore else */ - if (typeof listenerRef.current === 'function') { - listenerRef.current.apply(this, args); - } else if (typeof listenerRef.current!.handleEvent === 'function') { - listenerRef.current!.handleEvent.apply(this, args); - } - }, - - [] - ); - - useEffect(() => { - const tgt = - target && hasOwnProperty(target, 'current') ? (target as RefObject).current : target; - if (!tgt) return; - - const restParams: unknown[] = params.slice(2); - - on(tgt, params[0], eventListener, ...restParams); - - return () => { - off(tgt, params[0], eventListener, ...restParams); - }; - }, [target, params[0]]); + const isMounted = useIsMounted(); + + // Create static event listener + const listenerRef = useSyncedRef(params[1]); + const eventListener = useMemo( + () => + // As some event listeners designed to be used through `this` + // it is better to make listener a conventional function as it + // infers call context + + function (this: T, ...args) { + // Normally, such situation should not happen, but better to + // have back covered + /* istanbul ignore next */ + if (!isMounted()) return; + + // We dont care if non-listener provided, simply dont do anything + /* istanbul ignore else */ + if (typeof listenerRef.current === 'function') { + listenerRef.current.apply(this, args); + } else if (typeof listenerRef.current!.handleEvent === 'function') { + listenerRef.current!.handleEvent.apply(this, args); + } + }, + + [] + ); + + useEffect(() => { + const tgt = + target && hasOwnProperty(target, 'current') ? (target as RefObject).current : target; + if (!tgt) return; + + const restParams: unknown[] = params.slice(2); + + on(tgt, params[0], eventListener, ...restParams); + + return () => { + off(tgt, params[0], eventListener, ...restParams); + }; + }, [target, params[0]]); } diff --git a/src/useFirstMountState/__docs__/example.stories.tsx b/src/useFirstMountState/__docs__/example.stories.tsx index 19a93c790..bc8d8bb5a 100644 --- a/src/useFirstMountState/__docs__/example.stories.tsx +++ b/src/useFirstMountState/__docs__/example.stories.tsx @@ -2,19 +2,19 @@ import * as React from 'react'; import { useFirstMountState, useRerender } from '../..'; export function Example() { - const isFirstMount = useFirstMountState(); - const rerender = useRerender(); + const isFirstMount = useFirstMountState(); + const rerender = useRerender(); - return ( -
-
{isFirstMount ? 'This is the first render.' : 'This is not the first render.'}
- -
- ); + return ( +
+
{isFirstMount ? 'This is the first render.' : 'This is not the first render.'}
+ +
+ ); } diff --git a/src/useFirstMountState/__docs__/story.mdx b/src/useFirstMountState/__docs__/story.mdx index 07cd94713..e29aa0f9e 100644 --- a/src/useFirstMountState/__docs__/story.mdx +++ b/src/useFirstMountState/__docs__/story.mdx @@ -11,7 +11,7 @@ Returns a boolean that is `true` only on first render. #### Example - + ## Reference diff --git a/src/useFirstMountState/__tests__/dom.ts b/src/useFirstMountState/__tests__/dom.ts index 6f5c36f63..f67039878 100644 --- a/src/useFirstMountState/__tests__/dom.ts +++ b/src/useFirstMountState/__tests__/dom.ts @@ -2,21 +2,21 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useFirstMountState } from '../..'; describe('useFirstMountState', () => { - it('should return true on first render', () => { - const { result } = renderHook(() => useFirstMountState()); + it('should return true on first render', () => { + const { result } = renderHook(() => useFirstMountState()); - expect(result.current).toBe(true); - }); + expect(result.current).toBe(true); + }); - it('should return false on second and next renders', () => { - const { result, rerender } = renderHook(() => useFirstMountState()); + it('should return false on second and next renders', () => { + const { result, rerender } = renderHook(() => useFirstMountState()); - expect(result.current).toBe(true); + expect(result.current).toBe(true); - rerender(); - expect(result.current).toBe(false); + rerender(); + expect(result.current).toBe(false); - rerender(); - expect(result.current).toBe(false); - }); + rerender(); + expect(result.current).toBe(false); + }); }); diff --git a/src/useFirstMountState/__tests__/ssr.ts b/src/useFirstMountState/__tests__/ssr.ts index 7c8117302..553530f6e 100644 --- a/src/useFirstMountState/__tests__/ssr.ts +++ b/src/useFirstMountState/__tests__/ssr.ts @@ -2,9 +2,9 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useFirstMountState } from '../..'; describe('useFirstMountState', () => { - it('should return true on first render', () => { - const { result } = renderHook(() => useFirstMountState()); + it('should return true on first render', () => { + const { result } = renderHook(() => useFirstMountState()); - expect(result.current).toBe(true); - }); + expect(result.current).toBe(true); + }); }); diff --git a/src/useFirstMountState/index.ts b/src/useFirstMountState/index.ts index fbba8e064..57c39ba55 100644 --- a/src/useFirstMountState/index.ts +++ b/src/useFirstMountState/index.ts @@ -4,11 +4,11 @@ import { useEffect, useRef } from 'react'; * Returns a boolean that is `true` only on first render. */ export function useFirstMountState(): boolean { - const isFirstMount = useRef(true); + const isFirstMount = useRef(true); - useEffect(() => { - isFirstMount.current = false; - }, []); + useEffect(() => { + isFirstMount.current = false; + }, []); - return isFirstMount.current; + return isFirstMount.current; } diff --git a/src/useFunctionalState/__docs__/story.mdx b/src/useFunctionalState/__docs__/story.mdx index 0869699d1..3058c9235 100644 --- a/src/useFunctionalState/__docs__/story.mdx +++ b/src/useFunctionalState/__docs__/story.mdx @@ -10,10 +10,10 @@ Like `useState` but instead of raw state, state getter returned. ```ts export function useFunctionalState( - initialState: S | (() => S) + initialState: S | (() => S) ): [() => S, React.Dispatch>]; export function useFunctionalState(): [ - () => S | undefined, - React.Dispatch> + () => S | undefined, + React.Dispatch> ]; ``` diff --git a/src/useFunctionalState/__tests__/dom.ts b/src/useFunctionalState/__tests__/dom.ts index d592c3337..1c81805fd 100644 --- a/src/useFunctionalState/__tests__/dom.ts +++ b/src/useFunctionalState/__tests__/dom.ts @@ -2,30 +2,30 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { useFunctionalState } from '../..'; describe('useFunctionalState', () => { - it('should be defined', () => { - expect(useFunctionalState).toBeDefined(); - }); + it('should be defined', () => { + expect(useFunctionalState).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useFunctionalState()); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useFunctionalState()); + expect(result.error).toBeUndefined(); + }); - it('should return proper values', () => { - const { result } = renderHook(() => useFunctionalState(1)); - expect(result.current[1]).toBeInstanceOf(Function); - expect(result.current[0]).toBeInstanceOf(Function); - }); + it('should return proper values', () => { + const { result } = renderHook(() => useFunctionalState(1)); + expect(result.current[1]).toBeInstanceOf(Function); + expect(result.current[0]).toBeInstanceOf(Function); + }); - it('should return state getter', () => { - const { result } = renderHook(() => useFunctionalState(1)); + it('should return state getter', () => { + const { result } = renderHook(() => useFunctionalState(1)); - expect(result.current[0]()).toBe(1); + expect(result.current[0]()).toBe(1); - act(() => { - result.current[1](2); - }); + act(() => { + result.current[1](2); + }); - expect(result.current[0]()).toBe(2); - }); + expect(result.current[0]()).toBe(2); + }); }); diff --git a/src/useFunctionalState/__tests__/ssr.ts b/src/useFunctionalState/__tests__/ssr.ts index c19e65c15..e154dbbf5 100644 --- a/src/useFunctionalState/__tests__/ssr.ts +++ b/src/useFunctionalState/__tests__/ssr.ts @@ -2,18 +2,18 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useFunctionalState } from '../..'; describe('useFunctionalState', () => { - it('should be defined', () => { - expect(useFunctionalState).toBeDefined(); - }); + it('should be defined', () => { + expect(useFunctionalState).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useFunctionalState()); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useFunctionalState()); + expect(result.error).toBeUndefined(); + }); - it('should return proper values', () => { - const { result } = renderHook(() => useFunctionalState(1)); - expect(result.current[1]).toBeInstanceOf(Function); - expect(result.current[0]).toBeInstanceOf(Function); - }); + it('should return proper values', () => { + const { result } = renderHook(() => useFunctionalState(1)); + expect(result.current[1]).toBeInstanceOf(Function); + expect(result.current[0]).toBeInstanceOf(Function); + }); }); diff --git a/src/useFunctionalState/index.ts b/src/useFunctionalState/index.ts index a621af5c7..3b6599fe8 100644 --- a/src/useFunctionalState/index.ts +++ b/src/useFunctionalState/index.ts @@ -2,21 +2,21 @@ import { type Dispatch, type SetStateAction, useCallback, useState } from 'react import { useSyncedRef } from '../useSyncedRef'; export function useFunctionalState( - initialState: S | (() => S) + initialState: S | (() => S) ): [() => S, Dispatch>]; export function useFunctionalState(): [ - () => S | undefined, - Dispatch> + () => S | undefined, + Dispatch> ]; /** * Like `useState` but instead of raw state, state getter returned. */ export function useFunctionalState( - initialState?: S | (() => S) + initialState?: S | (() => S) ): [() => S | undefined, Dispatch>] { - const [state, setState] = useState(initialState); - const stateRef = useSyncedRef(state); + const [state, setState] = useState(initialState); + const stateRef = useSyncedRef(state); - return [useCallback(() => stateRef.current, []), setState]; + return [useCallback(() => stateRef.current, []), setState]; } diff --git a/src/useHookableRef/__docs__/example.stories.tsx b/src/useHookableRef/__docs__/example.stories.tsx index 84f77b888..4be93dfd6 100644 --- a/src/useHookableRef/__docs__/example.stories.tsx +++ b/src/useHookableRef/__docs__/example.stories.tsx @@ -3,43 +3,43 @@ import { useState } from 'react'; import { useHookableRef } from '../..'; export function Example() { - const [get, setGet] = useState(); - const [set, setSet] = useState(); + const [get, setGet] = useState(); + const [set, setSet] = useState(); - const ref = useHookableRef( - 123, - (v) => { - setSet(new Date()); - return v; - }, - (v) => { - setGet(new Date()); - return v; - } - ); + const ref = useHookableRef( + 123, + (v) => { + setSet(new Date()); + return v; + }, + (v) => { + setGet(new Date()); + return v; + } + ); - return ( -
-
Ref value read: {get?.toString()}
-
Ref value assign: {set?.toString()}
+ return ( +
+
Ref value read: {get?.toString()}
+
Ref value assign: {set?.toString()}
-
- - -
-
- ); +
+ + +
+
+ ); } diff --git a/src/useHookableRef/__docs__/story.mdx b/src/useHookableRef/__docs__/story.mdx index 42049d5a6..2588db878 100644 --- a/src/useHookableRef/__docs__/story.mdx +++ b/src/useHookableRef/__docs__/story.mdx @@ -11,7 +11,7 @@ Like `React.useRef` but it is possible to define get and set handlers. #### Example - + ## Reference @@ -20,9 +20,9 @@ Like `React.useRef` but it is possible to define get and set handlers. export type HookableRefHandler = (v: T) => T; export function useHookableRef( - initialValue: T, - onSet?: HookableRefHandler, - onGet?: HookableRefHandler + initialValue: T, + onSet?: HookableRefHandler, + onGet?: HookableRefHandler ): React.MutableRefObject; export function useHookableRef(): React.MutableRefObject; ``` diff --git a/src/useHookableRef/__tests__/dom.ts b/src/useHookableRef/__tests__/dom.ts index a4fdfbf8a..85c671022 100644 --- a/src/useHookableRef/__tests__/dom.ts +++ b/src/useHookableRef/__tests__/dom.ts @@ -2,54 +2,54 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useHookableRef } from '../..'; describe('useHookableRef', () => { - it('should be defined', () => { - expect(useHookableRef).toBeDefined(); - }); + it('should be defined', () => { + expect(useHookableRef).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useHookableRef()); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useHookableRef()); + expect(result.error).toBeUndefined(); + }); - it('should return ref object with initial value', () => { - const { result } = renderHook(() => useHookableRef(123)); - expect(result.current).toEqual({ current: 123 }); - }); + it('should return ref object with initial value', () => { + const { result } = renderHook(() => useHookableRef(123)); + expect(result.current).toEqual({ current: 123 }); + }); - it('should persist same reference between re-renders', () => { - const { result, rerender } = renderHook(() => useHookableRef(123)); - const firstResult = result.current; + it('should persist same reference between re-renders', () => { + const { result, rerender } = renderHook(() => useHookableRef(123)); + const firstResult = result.current; - rerender(); - expect(result.current).toBe(firstResult); + rerender(); + expect(result.current).toBe(firstResult); - rerender(); - expect(result.current).toBe(firstResult); - }); + rerender(); + expect(result.current).toBe(firstResult); + }); - it('should call getter and setter hook', () => { - const getter = jest.fn((v: number) => v); - const setter = jest.fn((v: number) => v); + it('should call getter and setter hook', () => { + const getter = jest.fn((v: number) => v); + const setter = jest.fn((v: number) => v); - const { result } = renderHook(() => useHookableRef(123, setter, getter)); + const { result } = renderHook(() => useHookableRef(123, setter, getter)); - expect(getter).not.toHaveBeenCalled(); - expect(setter).not.toHaveBeenCalled(); + expect(getter).not.toHaveBeenCalled(); + expect(setter).not.toHaveBeenCalled(); - expect(result.current.current).toBe(123); - expect(getter).toHaveBeenCalledTimes(1); + expect(result.current.current).toBe(123); + expect(getter).toHaveBeenCalledTimes(1); - result.current.current = 321; - expect(result.current.current).toBe(321); - expect(getter).toHaveBeenCalledTimes(2); - expect(setter).toHaveBeenCalledTimes(1); - }); + result.current.current = 321; + expect(result.current.current).toBe(321); + expect(getter).toHaveBeenCalledTimes(2); + expect(setter).toHaveBeenCalledTimes(1); + }); - it('should work properly without getter and setter', () => { - const { result } = renderHook(() => useHookableRef(123)); - expect(result.current.current).toBe(123); + it('should work properly without getter and setter', () => { + const { result } = renderHook(() => useHookableRef(123)); + expect(result.current.current).toBe(123); - result.current.current = 321; - expect(result.current.current).toBe(321); - }); + result.current.current = 321; + expect(result.current.current).toBe(321); + }); }); diff --git a/src/useHookableRef/__tests__/ssr.ts b/src/useHookableRef/__tests__/ssr.ts index a36ba43b4..3ba9ff5f9 100644 --- a/src/useHookableRef/__tests__/ssr.ts +++ b/src/useHookableRef/__tests__/ssr.ts @@ -2,12 +2,12 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useHookableRef } from '../..'; describe('useHookableRef', () => { - it('should be defined', () => { - expect(useHookableRef).toBeDefined(); - }); + it('should be defined', () => { + expect(useHookableRef).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useHookableRef()); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useHookableRef()); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useHookableRef/index.ts b/src/useHookableRef/index.ts index f20a45c1c..5acac4dd8 100644 --- a/src/useHookableRef/index.ts +++ b/src/useHookableRef/index.ts @@ -4,9 +4,9 @@ import { useSyncedRef } from '../useSyncedRef'; export type HookableRefHandler = (v: T) => T; export function useHookableRef( - initialValue: T, - onSet?: HookableRefHandler, - onGet?: HookableRefHandler + initialValue: T, + onSet?: HookableRefHandler, + onGet?: HookableRefHandler ): MutableRefObject; export function useHookableRef(): MutableRefObject; @@ -20,24 +20,24 @@ export function useHookableRef(): MutableRefObject * value will be used as a return value. */ export function useHookableRef( - initialValue?: T, - onSet?: HookableRefHandler, - onGet?: HookableRefHandler + initialValue?: T, + onSet?: HookableRefHandler, + onGet?: HookableRefHandler ): MutableRefObject { - const onSetRef = useSyncedRef(onSet); - const onGetRef = useSyncedRef(onGet); + const onSetRef = useSyncedRef(onSet); + const onGetRef = useSyncedRef(onGet); - return useMemo(() => { - let v = initialValue; + return useMemo(() => { + let v = initialValue; - return { - get current() { - return onGetRef.current === undefined ? v : onGetRef.current(v as T); - }, + return { + get current() { + return onGetRef.current === undefined ? v : onGetRef.current(v as T); + }, - set current(val) { - v = onSetRef.current === undefined ? val : onSetRef.current(val as T); - }, - }; - }, []); + set current(val) { + v = onSetRef.current === undefined ? val : onSetRef.current(val as T); + }, + }; + }, []); } diff --git a/src/useIntersectionObserver/__docs__/example.stories.tsx b/src/useIntersectionObserver/__docs__/example.stories.tsx index f296ec48a..3780e5e4c 100644 --- a/src/useIntersectionObserver/__docs__/example.stories.tsx +++ b/src/useIntersectionObserver/__docs__/example.stories.tsx @@ -3,48 +3,48 @@ import { useRef } from 'react'; import { useIntersectionObserver } from '../..'; export function Example() { - const rootRef = useRef(null); - const elementRef = useRef(null); - const intersection = useIntersectionObserver(elementRef, { root: rootRef, threshold: [0, 0.5] }); + const rootRef = useRef(null); + const elementRef = useRef(null); + const intersection = useIntersectionObserver(elementRef, { root: rootRef, threshold: [0, 0.5] }); - return ( -
-
- Below scrollable container holds a rectangle that turns green when 50% or more of it is - visible. -
+ return ( +
+
+ Below scrollable container holds a rectangle that turns green when 50% or more of it is + visible. +
-
-
= 0.5 ? 'green' : 'red', - width: '10vw', - height: '10vw', - margin: '39vh auto', - }} - /> -
-
-        {JSON.stringify(
-          {
-            boundingClientRect: intersection?.boundingClientRect,
-            intersectionRatio: intersection?.intersectionRatio,
-            intersectionRect: intersection?.intersectionRect,
-            isIntersecting: intersection?.isIntersecting,
-            rootBounds: intersection?.rootBounds,
-            time: intersection?.time,
-          },
-          null,
-          2
-        )}
-      
-
- ); +
+
= 0.5 ? 'green' : 'red', + width: '10vw', + height: '10vw', + margin: '39vh auto', + }} + /> +
+
+				{JSON.stringify(
+					{
+						boundingClientRect: intersection?.boundingClientRect,
+						intersectionRatio: intersection?.intersectionRatio,
+						intersectionRect: intersection?.intersectionRect,
+						isIntersecting: intersection?.isIntersecting,
+						rootBounds: intersection?.rootBounds,
+						time: intersection?.time,
+					},
+					null,
+					2
+				)}
+			
+
+ ); } diff --git a/src/useIntersectionObserver/__docs__/story.mdx b/src/useIntersectionObserver/__docs__/story.mdx index 8ef4f1f6a..331e97c79 100644 --- a/src/useIntersectionObserver/__docs__/story.mdx +++ b/src/useIntersectionObserver/__docs__/story.mdx @@ -17,15 +17,15 @@ viewport. #### Example - + ## Reference ```ts export function useIntersectionObserver( - target: RefObject | T | null, - { threshold = [0], root, rootMargin = '0px' }: IUseIntersectionObserverOptions = {} + target: RefObject | T | null, + { threshold = [0], root, rootMargin = '0px' }: IUseIntersectionObserverOptions = {} ): IntersectionObserverEntry | undefined; ``` diff --git a/src/useIntersectionObserver/__tests__/dom.ts b/src/useIntersectionObserver/__tests__/dom.ts index 13d3b4be9..3f33fcbe9 100644 --- a/src/useIntersectionObserver/__tests__/dom.ts +++ b/src/useIntersectionObserver/__tests__/dom.ts @@ -3,134 +3,134 @@ import { useIntersectionObserver } from '../..'; import Mock = jest.Mock; describe('useIntersectionObserver', () => { - let IntersectionObserverSpy: Mock; - const initialRO = global.ResizeObserver; - - beforeAll(() => { - IntersectionObserverSpy = jest.fn(() => ({ - observe: jest.fn(), - unobserve: jest.fn(), - disconnect: jest.fn(), - takeRecords: () => [], - root: document, - rootMargin: '0px', - thresholds: [0], - })); - - global.IntersectionObserver = IntersectionObserverSpy; - jest.useFakeTimers(); - }); - - beforeEach(() => { - IntersectionObserverSpy.mockClear(); - }); - - afterAll(() => { - global.ResizeObserver = initialRO; - jest.useRealTimers(); - }); - - it('should be defined', () => { - expect(useIntersectionObserver).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useIntersectionObserver(null)); - expect(result.error).toBeUndefined(); - }); - - it('should return undefined on first render', () => { - const div1 = document.createElement('div'); - const { result } = renderHook(() => useIntersectionObserver(div1)); - expect(result.current).toBeUndefined(); - }); - - it('should create IntersectionObserver instance only for unique set of options', () => { - expect(IntersectionObserverSpy).toHaveBeenCalledTimes(0); - const div1 = document.createElement('div'); - const div2 = document.createElement('div'); - - renderHook(() => useIntersectionObserver(div1)); - renderHook(() => useIntersectionObserver(div2)); - - expect(IntersectionObserverSpy).toHaveBeenCalledTimes(1); - }); - - it('should return intersection entry', () => { - const div1 = document.createElement('div'); - const div1Ref = { current: div1 }; - const div2 = document.createElement('div'); - - const { result: res1 } = renderHook(() => useIntersectionObserver(div1Ref)); - const { result: res2, unmount } = renderHook(() => - useIntersectionObserver(div2, { threshold: [0, 1] }) - ); - - expect(res1.current).toBeUndefined(); - expect(res2.current).toBeUndefined(); - - const entry1 = { target: div1 }; - const entry2 = { target: div2 }; - - act(() => { - IntersectionObserverSpy.mock.calls[0][0]([entry1]); - IntersectionObserverSpy.mock.calls[1][0]([entry2]); - jest.advanceTimersByTime(1); - }); - - expect(res1.current).toBe(entry1); - expect(res2.current).toBe(entry2); - - unmount(); - - const entry3 = { target: div1 }; - act(() => { - IntersectionObserverSpy.mock.calls[0][0]([entry3]); - jest.advanceTimersByTime(1); - }); - - expect(res1.current).toBe(entry3); - }); - - it('two hooks observing same target should use single observer', () => { - const div1 = document.createElement('div'); - const div2 = document.createElement('div'); - - const { result: res1 } = renderHook(() => - useIntersectionObserver(div1, { root: { current: div2 } }) - ); - const { result: res2 } = renderHook(() => - useIntersectionObserver(div1, { root: { current: div2 } }) - ); - - expect(res1.current).toBeUndefined(); - expect(res2.current).toBeUndefined(); - - const entry1 = { target: div1 }; - - act(() => { - IntersectionObserverSpy.mock.calls[0][0]([entry1]); - jest.advanceTimersByTime(1); - }); - - expect(res1.current).toBe(entry1); - expect(res2.current).toBe(entry1); - }); + let IntersectionObserverSpy: Mock; + const initialRO = global.ResizeObserver; + + beforeAll(() => { + IntersectionObserverSpy = jest.fn(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), + takeRecords: () => [], + root: document, + rootMargin: '0px', + thresholds: [0], + })); + + global.IntersectionObserver = IntersectionObserverSpy; + jest.useFakeTimers(); + }); + + beforeEach(() => { + IntersectionObserverSpy.mockClear(); + }); + + afterAll(() => { + global.ResizeObserver = initialRO; + jest.useRealTimers(); + }); + + it('should be defined', () => { + expect(useIntersectionObserver).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useIntersectionObserver(null)); + expect(result.error).toBeUndefined(); + }); + + it('should return undefined on first render', () => { + const div1 = document.createElement('div'); + const { result } = renderHook(() => useIntersectionObserver(div1)); + expect(result.current).toBeUndefined(); + }); + + it('should create IntersectionObserver instance only for unique set of options', () => { + expect(IntersectionObserverSpy).toHaveBeenCalledTimes(0); + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + + renderHook(() => useIntersectionObserver(div1)); + renderHook(() => useIntersectionObserver(div2)); + + expect(IntersectionObserverSpy).toHaveBeenCalledTimes(1); + }); + + it('should return intersection entry', () => { + const div1 = document.createElement('div'); + const div1Ref = { current: div1 }; + const div2 = document.createElement('div'); + + const { result: res1 } = renderHook(() => useIntersectionObserver(div1Ref)); + const { result: res2, unmount } = renderHook(() => + useIntersectionObserver(div2, { threshold: [0, 1] }) + ); + + expect(res1.current).toBeUndefined(); + expect(res2.current).toBeUndefined(); + + const entry1 = { target: div1 }; + const entry2 = { target: div2 }; + + act(() => { + IntersectionObserverSpy.mock.calls[0][0]([entry1]); + IntersectionObserverSpy.mock.calls[1][0]([entry2]); + jest.advanceTimersByTime(1); + }); + + expect(res1.current).toBe(entry1); + expect(res2.current).toBe(entry2); + + unmount(); + + const entry3 = { target: div1 }; + act(() => { + IntersectionObserverSpy.mock.calls[0][0]([entry3]); + jest.advanceTimersByTime(1); + }); + + expect(res1.current).toBe(entry3); + }); + + it('two hooks observing same target should use single observer', () => { + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + + const { result: res1 } = renderHook(() => + useIntersectionObserver(div1, { root: { current: div2 } }) + ); + const { result: res2 } = renderHook(() => + useIntersectionObserver(div1, { root: { current: div2 } }) + ); + + expect(res1.current).toBeUndefined(); + expect(res2.current).toBeUndefined(); + + const entry1 = { target: div1 }; + + act(() => { + IntersectionObserverSpy.mock.calls[0][0]([entry1]); + jest.advanceTimersByTime(1); + }); + + expect(res1.current).toBe(entry1); + expect(res2.current).toBe(entry1); + }); - it('should disconnect observer if last hook unmounted', () => { - const div1 = document.createElement('div'); + it('should disconnect observer if last hook unmounted', () => { + const div1 = document.createElement('div'); - const { result, unmount } = renderHook(() => useIntersectionObserver(div1)); - const entry1 = { target: div1 }; + const { result, unmount } = renderHook(() => useIntersectionObserver(div1)); + const entry1 = { target: div1 }; - act(() => { - IntersectionObserverSpy.mock.calls[0][0]([entry1]); - jest.advanceTimersByTime(1); - }); + act(() => { + IntersectionObserverSpy.mock.calls[0][0]([entry1]); + jest.advanceTimersByTime(1); + }); - expect(result.current).toBe(entry1); + expect(result.current).toBe(entry1); - unmount(); - expect(IntersectionObserverSpy.mock.results[0].value.disconnect).toHaveBeenCalled(); - }); + unmount(); + expect(IntersectionObserverSpy.mock.results[0].value.disconnect).toHaveBeenCalled(); + }); }); diff --git a/src/useIntersectionObserver/__tests__/ssr.ts b/src/useIntersectionObserver/__tests__/ssr.ts index 52bb16b8f..89ba16039 100644 --- a/src/useIntersectionObserver/__tests__/ssr.ts +++ b/src/useIntersectionObserver/__tests__/ssr.ts @@ -2,12 +2,12 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useIntersectionObserver } from '../..'; describe('useIntersectionObserver', () => { - it('should be defined', () => { - expect(useIntersectionObserver).toBeDefined(); - }); + it('should be defined', () => { + expect(useIntersectionObserver).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useIntersectionObserver(null)); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useIntersectionObserver(null)); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useIntersectionObserver/index.ts b/src/useIntersectionObserver/index.ts index 4989dbf7c..8aacbda7f 100644 --- a/src/useIntersectionObserver/index.ts +++ b/src/useIntersectionObserver/index.ts @@ -6,115 +6,115 @@ const DEFAULT_ROOT_MARGIN = '0px'; type IntersectionEntryCallback = (entry: IntersectionObserverEntry) => void; type ObserverEntry = { - observer: IntersectionObserver; - observe: (target: Element, callback: IntersectionEntryCallback) => void; - unobserve: (target: Element, callback: IntersectionEntryCallback) => void; + observer: IntersectionObserver; + observe: (target: Element, callback: IntersectionEntryCallback) => void; + unobserve: (target: Element, callback: IntersectionEntryCallback) => void; }; const observers = new Map>(); const getObserverEntry = (options: IntersectionObserverInit): ObserverEntry => { - const root = options.root ?? document; - - let rootObservers = observers.get(root); - - if (!rootObservers) { - rootObservers = new Map(); - observers.set(root, rootObservers); - } - - const opt = JSON.stringify([options.rootMargin, options.threshold]); - - let entry = rootObservers.get(opt); - - if (!entry) { - const callbacks = new Map>(); - - const observer = new IntersectionObserver((entries) => { - entries.forEach((e) => - callbacks.get(e.target)?.forEach((cb) => - setTimeout(() => { - cb(e); - }, 0) - ) - ); - }, options); - - entry = { - observer, - observe(target, callback) { - let cbs = callbacks.get(target); - - if (!cbs) { - // If target has no observers yet - register it - cbs = new Set(); - callbacks.set(target, cbs); - observer.observe(target); - } - - // As Set is duplicate-safe - simply add callback on each call - cbs.add(callback); - }, - unobserve(target, callback) { - const cbs = callbacks.get(target); - - // Else branch should never occur in case of normal execution - // because callbacks map is hidden in closure - it is impossible to - // simulate situation with non-existent `cbs` Set - /* istanbul ignore else */ - if (cbs) { - // Remove current observer - cbs.delete(callback); - - if (!cbs.size) { - // If no observers left unregister target completely - callbacks.delete(target); - observer.unobserve(target); - - // If not tracked elements left - disconnect observer - if (!callbacks.size) { - observer.disconnect(); - - rootObservers!.delete(opt); - - if (!rootObservers!.size) { - observers.delete(root); - } - } - } - } - }, - }; - - rootObservers.set(opt, entry); - } - - return entry; + const root = options.root ?? document; + + let rootObservers = observers.get(root); + + if (!rootObservers) { + rootObservers = new Map(); + observers.set(root, rootObservers); + } + + const opt = JSON.stringify([options.rootMargin, options.threshold]); + + let entry = rootObservers.get(opt); + + if (!entry) { + const callbacks = new Map>(); + + const observer = new IntersectionObserver((entries) => { + entries.forEach((e) => + callbacks.get(e.target)?.forEach((cb) => + setTimeout(() => { + cb(e); + }, 0) + ) + ); + }, options); + + entry = { + observer, + observe(target, callback) { + let cbs = callbacks.get(target); + + if (!cbs) { + // If target has no observers yet - register it + cbs = new Set(); + callbacks.set(target, cbs); + observer.observe(target); + } + + // As Set is duplicate-safe - simply add callback on each call + cbs.add(callback); + }, + unobserve(target, callback) { + const cbs = callbacks.get(target); + + // Else branch should never occur in case of normal execution + // because callbacks map is hidden in closure - it is impossible to + // simulate situation with non-existent `cbs` Set + /* istanbul ignore else */ + if (cbs) { + // Remove current observer + cbs.delete(callback); + + if (!cbs.size) { + // If no observers left unregister target completely + callbacks.delete(target); + observer.unobserve(target); + + // If not tracked elements left - disconnect observer + if (!callbacks.size) { + observer.disconnect(); + + rootObservers!.delete(opt); + + if (!rootObservers!.size) { + observers.delete(root); + } + } + } + } + }, + }; + + rootObservers.set(opt, entry); + } + + return entry; }; export type UseIntersectionObserverOptions = { - /** - * An Element or Document object (or its react reference) which is an - * ancestor of the intended target, whose bounding rectangle will be - * considered the viewport. Any part of the target not visible in the visible - * area of the root is not considered visible. - */ - root?: RefObject | Element | Document | null; - /** - * A string which specifies a set of offsets to add to the root's bounding_box - * when calculating intersections, effectively shrinking or growing the root - * for calculation purposes. The syntax is approximately the same as that for - * the CSS margin property; The default is `0px`. - */ - rootMargin?: string; - /** - * Array of numbers between 0.0 and 1.0, specifying a ratio of intersection - * area to total bounding box area for the observed target. A value of 0.0 - * means that even a single visible pixel counts as the target being visible. - * 1.0 means that the entire target element is visible. - * The default is a threshold of `[0]`. - */ - threshold?: number[]; + /** + * An Element or Document object (or its react reference) which is an + * ancestor of the intended target, whose bounding rectangle will be + * considered the viewport. Any part of the target not visible in the visible + * area of the root is not considered visible. + */ + root?: RefObject | Element | Document | null; + /** + * A string which specifies a set of offsets to add to the root's bounding_box + * when calculating intersections, effectively shrinking or growing the root + * for calculation purposes. The syntax is approximately the same as that for + * the CSS margin property; The default is `0px`. + */ + rootMargin?: string; + /** + * Array of numbers between 0.0 and 1.0, specifying a ratio of intersection + * area to total bounding box area for the observed target. A value of 0.0 + * means that even a single visible pixel counts as the target being visible. + * 1.0 means that the entire target element is visible. + * The default is a threshold of `[0]`. + */ + threshold?: number[]; }; /** @@ -126,42 +126,42 @@ export type UseIntersectionObserverOptions = { * react reference */ export function useIntersectionObserver( - target: RefObject | T | null, - { - threshold = DEFAULT_THRESHOLD, - root: r, - rootMargin = DEFAULT_ROOT_MARGIN, - }: UseIntersectionObserverOptions = {} + target: RefObject | T | null, + { + threshold = DEFAULT_THRESHOLD, + root: r, + rootMargin = DEFAULT_ROOT_MARGIN, + }: UseIntersectionObserverOptions = {} ): IntersectionObserverEntry | undefined { - const [state, setState] = useState(); - - useEffect(() => { - const tgt = target && 'current' in target ? target.current : target; - if (!tgt) return; - - let subscribed = true; - const observerEntry = getObserverEntry({ - root: r && 'current' in r ? r.current : r, - rootMargin, - threshold, - }); - - const handler: IntersectionEntryCallback = (entry) => { - // It is reinsurance for the highly asynchronous invocations, almost - // impossible to achieve in tests, thus excluding from LOC - /* istanbul ignore else */ - if (subscribed) { - setState(entry); - } - }; - - observerEntry.observe(tgt, handler); - - return () => { - subscribed = false; - observerEntry.unobserve(tgt, handler); - }; - }, [target, r, rootMargin, ...threshold]); - - return state; + const [state, setState] = useState(); + + useEffect(() => { + const tgt = target && 'current' in target ? target.current : target; + if (!tgt) return; + + let subscribed = true; + const observerEntry = getObserverEntry({ + root: r && 'current' in r ? r.current : r, + rootMargin, + threshold, + }); + + const handler: IntersectionEntryCallback = (entry) => { + // It is reinsurance for the highly asynchronous invocations, almost + // impossible to achieve in tests, thus excluding from LOC + /* istanbul ignore else */ + if (subscribed) { + setState(entry); + } + }; + + observerEntry.observe(tgt, handler); + + return () => { + subscribed = false; + observerEntry.unobserve(tgt, handler); + }; + }, [target, r, rootMargin, ...threshold]); + + return state; } diff --git a/src/useIntervalEffect/__docs__/example.stories.tsx b/src/useIntervalEffect/__docs__/example.stories.tsx index 2a149c63f..0d6993800 100644 --- a/src/useIntervalEffect/__docs__/example.stories.tsx +++ b/src/useIntervalEffect/__docs__/example.stories.tsx @@ -3,27 +3,27 @@ import { useState } from 'react'; import { useIntervalEffect, useToggle } from '../..'; export function Example() { - const [state, setState] = useState(); - const [enabled, toggleEnabled] = useToggle(); + const [state, setState] = useState(); + const [enabled, toggleEnabled] = useToggle(); - useIntervalEffect( - () => { - setState(new Date()); - }, - enabled ? 1000 : undefined - ); + useIntervalEffect( + () => { + setState(new Date()); + }, + enabled ? 1000 : undefined + ); - return ( -
- Last interval invocation: {state?.toString()} -
- -
- ); + return ( +
+ Last interval invocation: {state?.toString()} +
+ +
+ ); } diff --git a/src/useIntervalEffect/__docs__/story.mdx b/src/useIntervalEffect/__docs__/story.mdx index 7a9133840..8e2db4dc2 100644 --- a/src/useIntervalEffect/__docs__/story.mdx +++ b/src/useIntervalEffect/__docs__/story.mdx @@ -16,7 +16,7 @@ Like `setInterval` but in the form of a React hook. #### Example - + ## Reference diff --git a/src/useIntervalEffect/__tests__/dom.ts b/src/useIntervalEffect/__tests__/dom.ts index 5f7ff1d74..8bd497d4c 100644 --- a/src/useIntervalEffect/__tests__/dom.ts +++ b/src/useIntervalEffect/__tests__/dom.ts @@ -3,86 +3,86 @@ import { useIntervalEffect } from '../..'; import advanceTimersByTime = jest.advanceTimersByTime; describe('useIntervalEffect', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - - beforeEach(() => { - jest.clearAllTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - it('should be defined', () => { - expect(useIntervalEffect).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => { - useIntervalEffect(() => {}, 123); - }); - expect(result.error).toBeUndefined(); - }); - - it('should set interval and cancel it on unmount', () => { - const spy = jest.fn(); - const { unmount } = renderHook(() => { - useIntervalEffect(spy, 100); - }); - - jest.advanceTimersByTime(99); - expect(spy).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(1); - expect(spy).toHaveBeenCalledTimes(1); - - jest.advanceTimersByTime(300); - expect(spy).toHaveBeenCalledTimes(4); - - unmount(); - expect(spy).toHaveBeenCalledTimes(4); - }); - - it('should reset interval in delay change', () => { - const spy = jest.fn(); - const { rerender } = renderHook( - ({ delay }) => { - useIntervalEffect(spy, delay); - }, - { - initialProps: { delay: 100 }, - } - ); - - advanceTimersByTime(99); - expect(spy).not.toHaveBeenCalled(); - - rerender({ delay: 50 }); - advanceTimersByTime(49); - expect(spy).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(1); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it('should cancel interval if delay is undefined', () => { - const spy = jest.fn(); - const { rerender } = renderHook<{ delay: number | undefined }, void>( - ({ delay }) => { - useIntervalEffect(spy, delay); - }, - { - initialProps: { delay: 100 }, - } - ); - - advanceTimersByTime(99); - expect(spy).not.toHaveBeenCalled(); - - rerender({ delay: undefined }); - advanceTimersByTime(2000); - expect(spy).not.toHaveBeenCalled(); - }); + beforeAll(() => { + jest.useFakeTimers(); + }); + + beforeEach(() => { + jest.clearAllTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should be defined', () => { + expect(useIntervalEffect).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => { + useIntervalEffect(() => {}, 123); + }); + expect(result.error).toBeUndefined(); + }); + + it('should set interval and cancel it on unmount', () => { + const spy = jest.fn(); + const { unmount } = renderHook(() => { + useIntervalEffect(spy, 100); + }); + + jest.advanceTimersByTime(99); + expect(spy).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1); + expect(spy).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(300); + expect(spy).toHaveBeenCalledTimes(4); + + unmount(); + expect(spy).toHaveBeenCalledTimes(4); + }); + + it('should reset interval in delay change', () => { + const spy = jest.fn(); + const { rerender } = renderHook( + ({ delay }) => { + useIntervalEffect(spy, delay); + }, + { + initialProps: { delay: 100 }, + } + ); + + advanceTimersByTime(99); + expect(spy).not.toHaveBeenCalled(); + + rerender({ delay: 50 }); + advanceTimersByTime(49); + expect(spy).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should cancel interval if delay is undefined', () => { + const spy = jest.fn(); + const { rerender } = renderHook<{ delay: number | undefined }, void>( + ({ delay }) => { + useIntervalEffect(spy, delay); + }, + { + initialProps: { delay: 100 }, + } + ); + + advanceTimersByTime(99); + expect(spy).not.toHaveBeenCalled(); + + rerender({ delay: undefined }); + advanceTimersByTime(2000); + expect(spy).not.toHaveBeenCalled(); + }); }); diff --git a/src/useIntervalEffect/__tests__/ssr.ts b/src/useIntervalEffect/__tests__/ssr.ts index a9a7b20b5..910a4793b 100644 --- a/src/useIntervalEffect/__tests__/ssr.ts +++ b/src/useIntervalEffect/__tests__/ssr.ts @@ -2,14 +2,14 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useIntervalEffect } from '../..'; describe('useIntervalEffect', () => { - it('should be defined', () => { - expect(useIntervalEffect).toBeDefined(); - }); + it('should be defined', () => { + expect(useIntervalEffect).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useIntervalEffect(() => {}, 123); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useIntervalEffect(() => {}, 123); + }); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useIntervalEffect/index.ts b/src/useIntervalEffect/index.ts index dbd275cd4..334cde1d7 100644 --- a/src/useIntervalEffect/index.ts +++ b/src/useIntervalEffect/index.ts @@ -9,19 +9,19 @@ import { useSyncedRef } from '../useSyncedRef'; * be cancelled. Keep in mind, that changing this parameter will reset the interval. */ export function useIntervalEffect(callback: () => void, ms?: number): void { - const cbRef = useSyncedRef(callback); + const cbRef = useSyncedRef(callback); - useEffect(() => { - if (!ms && ms !== 0) { - return; - } + useEffect(() => { + if (!ms && ms !== 0) { + return; + } - const id = setInterval(() => { - cbRef.current(); - }, ms); + const id = setInterval(() => { + cbRef.current(); + }, ms); - return () => { - clearInterval(id); - }; - }, [ms]); + return () => { + clearInterval(id); + }; + }, [ms]); } diff --git a/src/useIsMounted/__docs__/example.stories.tsx b/src/useIsMounted/__docs__/example.stories.tsx index 5b8069fe1..b0e2e251a 100644 --- a/src/useIsMounted/__docs__/example.stories.tsx +++ b/src/useIsMounted/__docs__/example.stories.tsx @@ -2,47 +2,47 @@ import * as React from 'react'; import { useIsMounted, useMountEffect, useToggle } from '../..'; export function Example() { - const [isToggled, toggle] = useToggle(false); + const [isToggled, toggle] = useToggle(false); - function ToggledComponent() { - const isMounted = useIsMounted(); + function ToggledComponent() { + const isMounted = useIsMounted(); - // As you can see, below effect has no dependencies, it will be executed - // anyway, but alert will be displayed only in case component persist mounted - useMountEffect(() => { - setTimeout(() => { - if (isMounted()) { - // eslint-disable-next-line no-alert - alert('Component was not unmounted!'); - } - }, 5000); - }); + // As you can see, below effect has no dependencies, it will be executed + // anyway, but alert will be displayed only in case component persist mounted + useMountEffect(() => { + setTimeout(() => { + if (isMounted()) { + // eslint-disable-next-line no-alert + alert('Component was not unmounted!'); + } + }, 5000); + }); - return ( -

- This component will display an alert 5 seconds after mount. -
- Unmounting the component will prevent it. -

- ); - } + return ( +

+ This component will display an alert 5 seconds after mount. +
+ Unmounting the component will prevent it. +

+ ); + } - return ( -
- {!isToggled && ( -
- Because the example component displays an alert without interaction, it is initially - unmounted. -
- )} - {' '} - {isToggled && } -
- ); + return ( +
+ {!isToggled && ( +
+ Because the example component displays an alert without interaction, it is initially + unmounted. +
+ )} + {' '} + {isToggled && } +
+ ); } diff --git a/src/useIsMounted/__docs__/story.mdx b/src/useIsMounted/__docs__/story.mdx index 5cdc37868..14c4b6fdd 100644 --- a/src/useIsMounted/__docs__/story.mdx +++ b/src/useIsMounted/__docs__/story.mdx @@ -14,7 +14,7 @@ component mount state within async effects. #### Example - + ## Reference diff --git a/src/useIsMounted/__tests__/dom.ts b/src/useIsMounted/__tests__/dom.ts index 7a1d40419..c4dd82d70 100644 --- a/src/useIsMounted/__tests__/dom.ts +++ b/src/useIsMounted/__tests__/dom.ts @@ -2,51 +2,51 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useIsMounted } from '../..'; describe('useIsMounted', () => { - it('should be defined', () => { - expect(useIsMounted).toBeDefined(); - }); + it('should be defined', () => { + expect(useIsMounted).toBeDefined(); + }); - it('should return a function', () => { - const { result } = renderHook(() => useIsMounted()); + it('should return a function', () => { + const { result } = renderHook(() => useIsMounted()); - expect(result.current).toBeInstanceOf(Function); - }); + expect(result.current).toBeInstanceOf(Function); + }); - it('should return false within first render', () => { - const { result } = renderHook(() => { - const isMounted = useIsMounted(); - return isMounted(); - }); + it('should return false within first render', () => { + const { result } = renderHook(() => { + const isMounted = useIsMounted(); + return isMounted(); + }); - expect(result.current).toBe(false); - }); + expect(result.current).toBe(false); + }); - it('should return true after mount', () => { - const { result } = renderHook(() => useIsMounted()); + it('should return true after mount', () => { + const { result } = renderHook(() => useIsMounted()); - expect(result.current()).toBe(true); - }); + expect(result.current()).toBe(true); + }); - it('should return same function on each render', () => { - const { result, rerender } = renderHook(() => useIsMounted()); + it('should return same function on each render', () => { + const { result, rerender } = renderHook(() => useIsMounted()); - const fn1 = result.current; - rerender(); - const fn2 = result.current; - rerender(); - const fn3 = result.current; + const fn1 = result.current; + rerender(); + const fn2 = result.current; + rerender(); + const fn3 = result.current; - expect(fn1).toBe(fn2); - expect(fn2).toBe(fn3); - }); + expect(fn1).toBe(fn2); + expect(fn2).toBe(fn3); + }); - it('should return false after component unmount', () => { - const { result, unmount } = renderHook(() => useIsMounted()); + it('should return false after component unmount', () => { + const { result, unmount } = renderHook(() => useIsMounted()); - expect(result.current()).toBe(true); + expect(result.current()).toBe(true); - unmount(); + unmount(); - expect(result.current()).toBe(false); - }); + expect(result.current()).toBe(false); + }); }); diff --git a/src/useIsMounted/__tests__/ssr.ts b/src/useIsMounted/__tests__/ssr.ts index 718a946ae..5343fa032 100644 --- a/src/useIsMounted/__tests__/ssr.ts +++ b/src/useIsMounted/__tests__/ssr.ts @@ -2,28 +2,28 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useIsMounted } from '../..'; describe('useIsMounted', () => { - it('should be defined', () => { - expect(useIsMounted).toBeDefined(); - }); + it('should be defined', () => { + expect(useIsMounted).toBeDefined(); + }); - it('should return a function', () => { - const { result } = renderHook(() => useIsMounted()); + it('should return a function', () => { + const { result } = renderHook(() => useIsMounted()); - expect(result.current).toBeInstanceOf(Function); - }); + expect(result.current).toBeInstanceOf(Function); + }); - it('should return false within first render', () => { - const { result } = renderHook(() => { - const isMounted = useIsMounted(); - return isMounted(); - }); + it('should return false within first render', () => { + const { result } = renderHook(() => { + const isMounted = useIsMounted(); + return isMounted(); + }); - expect(result.current).toBe(false); - }); + expect(result.current).toBe(false); + }); - it('should return false after mount', () => { - const { result } = renderHook(() => useIsMounted()); + it('should return false after mount', () => { + const { result } = renderHook(() => useIsMounted()); - expect(result.current()).toBe(false); - }); + expect(result.current()).toBe(false); + }); }); diff --git a/src/useIsMounted/index.ts b/src/useIsMounted/index.ts index e403f166e..803b117ed 100644 --- a/src/useIsMounted/index.ts +++ b/src/useIsMounted/index.ts @@ -9,16 +9,16 @@ import { useCallback, useEffect, useRef } from 'react'; * @return Function that returns `true` only if the component is mounted. */ export function useIsMounted(initialValue = false): () => boolean { - const isMounted = useRef(initialValue); - const get = useCallback(() => isMounted.current, []); + const isMounted = useRef(initialValue); + const get = useCallback(() => isMounted.current, []); - useEffect(() => { - isMounted.current = true; + useEffect(() => { + isMounted.current = true; - return () => { - isMounted.current = false; - }; - }, []); + return () => { + isMounted.current = false; + }; + }, []); - return get; + return get; } diff --git a/src/useIsomorphicLayoutEffect/__tests__/dom.ts b/src/useIsomorphicLayoutEffect/__tests__/dom.ts index 8eb19533b..339f8dab8 100644 --- a/src/useIsomorphicLayoutEffect/__tests__/dom.ts +++ b/src/useIsomorphicLayoutEffect/__tests__/dom.ts @@ -2,11 +2,11 @@ import { useLayoutEffect } from 'react'; import { useIsomorphicLayoutEffect } from '../..'; describe('useIsomorphicLayoutEffect', () => { - it('should be defined', () => { - expect(useIsomorphicLayoutEffect).toBeDefined(); - }); + it('should be defined', () => { + expect(useIsomorphicLayoutEffect).toBeDefined(); + }); - it('should be equal `useLayoutEffect`', () => { - expect(useIsomorphicLayoutEffect).toBe(useLayoutEffect); - }); + it('should be equal `useLayoutEffect`', () => { + expect(useIsomorphicLayoutEffect).toBe(useLayoutEffect); + }); }); diff --git a/src/useIsomorphicLayoutEffect/__tests__/ssr.ts b/src/useIsomorphicLayoutEffect/__tests__/ssr.ts index e5e121b1c..526664073 100644 --- a/src/useIsomorphicLayoutEffect/__tests__/ssr.ts +++ b/src/useIsomorphicLayoutEffect/__tests__/ssr.ts @@ -2,11 +2,11 @@ import { useEffect } from 'react'; import { useIsomorphicLayoutEffect } from '../..'; describe('useIsomorphicLayoutEffect', () => { - it('should be defined', () => { - expect(useIsomorphicLayoutEffect).toBeDefined(); - }); + it('should be defined', () => { + expect(useIsomorphicLayoutEffect).toBeDefined(); + }); - it('should be equal `useEffect`', () => { - expect(useIsomorphicLayoutEffect).toBe(useEffect); - }); + it('should be equal `useEffect`', () => { + expect(useIsomorphicLayoutEffect).toBe(useEffect); + }); }); diff --git a/src/useKeyboardEvent/__docs__/example.stories.tsx b/src/useKeyboardEvent/__docs__/example.stories.tsx index f9e19a7cf..c9777967b 100644 --- a/src/useKeyboardEvent/__docs__/example.stories.tsx +++ b/src/useKeyboardEvent/__docs__/example.stories.tsx @@ -3,28 +3,28 @@ import { useState } from 'react'; import { useKeyboardEvent } from '../..'; export function Example() { - const [list, setList] = useState([]); + const [list, setList] = useState([]); - useKeyboardEvent( - true, - (ev) => { - setList((l) => [...l.slice(-10), ev.key]); - }, - [], - { eventOptions: { passive: true } } - ); + useKeyboardEvent( + true, + (ev) => { + setList((l) => [...l.slice(-10), ev.key]); + }, + [], + { eventOptions: { passive: true } } + ); - return ( -
-
Press any keys on the keyboard and they will appear below.
+ return ( +
+
Press any keys on the keyboard and they will appear below.
-

You have pressed

-
    - {list.map((k, i) => ( - // eslint-disable-next-line react/no-array-index-key -
  • {k}
  • - ))} -
-
- ); +

You have pressed

+
    + {list.map((k, i) => ( + // eslint-disable-next-line react/no-array-index-key +
  • {k}
  • + ))} +
+
+ ); } diff --git a/src/useKeyboardEvent/__docs__/story.mdx b/src/useKeyboardEvent/__docs__/story.mdx index 372600fd6..46f8f7577 100644 --- a/src/useKeyboardEvent/__docs__/story.mdx +++ b/src/useKeyboardEvent/__docs__/story.mdx @@ -11,23 +11,23 @@ Invokes a callback when a keyboard event occurs on the chosen target element. #### Example - + ## Reference ```ts type UseKeyboardEventOptions = { - event?: 'keydown' | 'keypress' | 'keyup'; - target?: RefObject | T | null; - eventOptions?: boolean | AddEventListenerOptions; + event?: 'keydown' | 'keypress' | 'keyup'; + target?: RefObject | T | null; + eventOptions?: boolean | AddEventListenerOptions; }; function useKeyboardEvent( - keyOrPredicate: KeyboardEventFilter, - callback: KeyboardEventHandler, - deps?: DependencyList, - options: UseKeyboardEventOptions = {} + keyOrPredicate: KeyboardEventFilter, + callback: KeyboardEventHandler, + deps?: DependencyList, + options: UseKeyboardEventOptions = {} ): void; ``` diff --git a/src/useKeyboardEvent/__tests__/dom.ts b/src/useKeyboardEvent/__tests__/dom.ts index a09ff4f27..c4216b623 100644 --- a/src/useKeyboardEvent/__tests__/dom.ts +++ b/src/useKeyboardEvent/__tests__/dom.ts @@ -2,171 +2,171 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { type KeyboardEventFilter, useKeyboardEvent } from '../..'; describe('useKeyboardEvent', () => { - it('should be defined', () => { - expect(useKeyboardEvent).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => { - useKeyboardEvent('a', () => {}); - }); - expect(result.error).toBeUndefined(); - }); - - it('should bind listener on mount and unbind on unmount', () => { - const div = document.createElement('div'); - const addSpy = jest.spyOn(div, 'addEventListener'); - const removeSpy = jest.spyOn(div, 'removeEventListener'); - - const { rerender, unmount } = renderHook(() => { - useKeyboardEvent( - () => true, - () => {}, - undefined, - { target: div, event: 'keydown', eventOptions: { passive: true } } - ); - }); - - expect(addSpy).toHaveBeenCalledTimes(1); - expect(removeSpy).toHaveBeenCalledTimes(0); - - rerender(); - expect(addSpy).toHaveBeenCalledTimes(1); - expect(removeSpy).toHaveBeenCalledTimes(0); - - unmount(); - expect(addSpy).toHaveBeenCalledTimes(1); - expect(removeSpy).toHaveBeenCalledTimes(1); - }); - - it('should work with react refs', () => { - const div = document.createElement('div'); - const addSpy = jest.spyOn(div, 'addEventListener'); - const removeSpy = jest.spyOn(div, 'removeEventListener'); - - const ref = { current: div }; - const { rerender, unmount } = renderHook(() => { - useKeyboardEvent( - () => true, - () => {}, - undefined, - { target: ref, eventOptions: { passive: true } } - ); - }); - - expect(addSpy).toHaveBeenCalledTimes(1); - expect(addSpy.mock.calls[0][2]).toStrictEqual({ passive: true }); - expect(removeSpy).toHaveBeenCalledTimes(0); - - rerender(); - expect(addSpy).toHaveBeenCalledTimes(1); - expect(removeSpy).toHaveBeenCalledTimes(0); - - unmount(); - expect(addSpy).toHaveBeenCalledTimes(1); - expect(removeSpy).toHaveBeenCalledTimes(1); - }); - - it('should invoke provided function on the event trigger with proper context', () => { - const div = document.createElement('div'); - let context: any; - const spy = jest.fn(function (this: any) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - context = this; - }); - - renderHook(() => { - useKeyboardEvent(() => true, spy, undefined, { - target: div, - event: 'keydown', - eventOptions: { passive: true }, - }); - }); - - const evt = new KeyboardEvent('keydown', { key: 'a' }); - div.dispatchEvent(evt); - expect(context).toBe(div); - - expect(spy).toHaveBeenCalledWith(evt); - div.dispatchEvent(new KeyboardEvent('keyup', { key: 'a' })); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it('should invoke provided function based on string key filter with proper context', () => { - const div = document.createElement('div'); - let context: any; - const spy = jest.fn(function (this: any) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - context = this; - }); - - renderHook(() => { - useKeyboardEvent('a', spy, undefined, { - target: div, - event: 'keydown', - eventOptions: { passive: true }, - }); - }); - - const evt = new KeyboardEvent('keydown', { key: 'a' }); - div.dispatchEvent(evt); - expect(spy).toHaveBeenCalledWith(evt); - expect(context).toBe(div); - - div.dispatchEvent(new KeyboardEvent('keydown', { key: 'b' })); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it('should invoke provided function based on function key filter with proper context', () => { - const div = document.createElement('div'); - let context: any; - const spy = jest.fn(function (this: any) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - context = this; - }); - - renderHook(() => { - useKeyboardEvent((ev) => ev.metaKey, spy, undefined, { - target: div, - event: 'keydown', - eventOptions: { passive: true }, - }); - }); - - const evt = new KeyboardEvent('keydown', { key: 'a', metaKey: true }); - div.dispatchEvent(evt); - expect(spy).toHaveBeenCalledWith(evt); - expect(context).toBe(div); - - div.dispatchEvent(new KeyboardEvent('keydown', { key: 'b' })); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it('should fallback to boolean when key filter is not function or string', () => { - const div = document.createElement('div'); - const spy = jest.fn(); - - const { unmount } = renderHook(() => { - useKeyboardEvent(null, spy, undefined, { - target: div, - event: 'keydown', - eventOptions: { passive: true }, - }); - }); - const evt = new KeyboardEvent('keydown', { key: 'a', metaKey: true }); - div.dispatchEvent(evt); - expect(spy).not.toHaveBeenCalledWith(evt); - unmount(); - - renderHook(() => { - useKeyboardEvent({} as KeyboardEventFilter, spy, undefined, { - target: div, - event: 'keydown', - eventOptions: { passive: true }, - }); - }); - - div.dispatchEvent(evt); - expect(spy).toHaveBeenCalledWith(evt); - }); + it('should be defined', () => { + expect(useKeyboardEvent).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => { + useKeyboardEvent('a', () => {}); + }); + expect(result.error).toBeUndefined(); + }); + + it('should bind listener on mount and unbind on unmount', () => { + const div = document.createElement('div'); + const addSpy = jest.spyOn(div, 'addEventListener'); + const removeSpy = jest.spyOn(div, 'removeEventListener'); + + const { rerender, unmount } = renderHook(() => { + useKeyboardEvent( + () => true, + () => {}, + undefined, + { target: div, event: 'keydown', eventOptions: { passive: true } } + ); + }); + + expect(addSpy).toHaveBeenCalledTimes(1); + expect(removeSpy).toHaveBeenCalledTimes(0); + + rerender(); + expect(addSpy).toHaveBeenCalledTimes(1); + expect(removeSpy).toHaveBeenCalledTimes(0); + + unmount(); + expect(addSpy).toHaveBeenCalledTimes(1); + expect(removeSpy).toHaveBeenCalledTimes(1); + }); + + it('should work with react refs', () => { + const div = document.createElement('div'); + const addSpy = jest.spyOn(div, 'addEventListener'); + const removeSpy = jest.spyOn(div, 'removeEventListener'); + + const ref = { current: div }; + const { rerender, unmount } = renderHook(() => { + useKeyboardEvent( + () => true, + () => {}, + undefined, + { target: ref, eventOptions: { passive: true } } + ); + }); + + expect(addSpy).toHaveBeenCalledTimes(1); + expect(addSpy.mock.calls[0][2]).toStrictEqual({ passive: true }); + expect(removeSpy).toHaveBeenCalledTimes(0); + + rerender(); + expect(addSpy).toHaveBeenCalledTimes(1); + expect(removeSpy).toHaveBeenCalledTimes(0); + + unmount(); + expect(addSpy).toHaveBeenCalledTimes(1); + expect(removeSpy).toHaveBeenCalledTimes(1); + }); + + it('should invoke provided function on the event trigger with proper context', () => { + const div = document.createElement('div'); + let context: any; + const spy = jest.fn(function (this: any) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + context = this; + }); + + renderHook(() => { + useKeyboardEvent(() => true, spy, undefined, { + target: div, + event: 'keydown', + eventOptions: { passive: true }, + }); + }); + + const evt = new KeyboardEvent('keydown', { key: 'a' }); + div.dispatchEvent(evt); + expect(context).toBe(div); + + expect(spy).toHaveBeenCalledWith(evt); + div.dispatchEvent(new KeyboardEvent('keyup', { key: 'a' })); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should invoke provided function based on string key filter with proper context', () => { + const div = document.createElement('div'); + let context: any; + const spy = jest.fn(function (this: any) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + context = this; + }); + + renderHook(() => { + useKeyboardEvent('a', spy, undefined, { + target: div, + event: 'keydown', + eventOptions: { passive: true }, + }); + }); + + const evt = new KeyboardEvent('keydown', { key: 'a' }); + div.dispatchEvent(evt); + expect(spy).toHaveBeenCalledWith(evt); + expect(context).toBe(div); + + div.dispatchEvent(new KeyboardEvent('keydown', { key: 'b' })); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should invoke provided function based on function key filter with proper context', () => { + const div = document.createElement('div'); + let context: any; + const spy = jest.fn(function (this: any) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + context = this; + }); + + renderHook(() => { + useKeyboardEvent((ev) => ev.metaKey, spy, undefined, { + target: div, + event: 'keydown', + eventOptions: { passive: true }, + }); + }); + + const evt = new KeyboardEvent('keydown', { key: 'a', metaKey: true }); + div.dispatchEvent(evt); + expect(spy).toHaveBeenCalledWith(evt); + expect(context).toBe(div); + + div.dispatchEvent(new KeyboardEvent('keydown', { key: 'b' })); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should fallback to boolean when key filter is not function or string', () => { + const div = document.createElement('div'); + const spy = jest.fn(); + + const { unmount } = renderHook(() => { + useKeyboardEvent(null, spy, undefined, { + target: div, + event: 'keydown', + eventOptions: { passive: true }, + }); + }); + const evt = new KeyboardEvent('keydown', { key: 'a', metaKey: true }); + div.dispatchEvent(evt); + expect(spy).not.toHaveBeenCalledWith(evt); + unmount(); + + renderHook(() => { + useKeyboardEvent({} as KeyboardEventFilter, spy, undefined, { + target: div, + event: 'keydown', + eventOptions: { passive: true }, + }); + }); + + div.dispatchEvent(evt); + expect(spy).toHaveBeenCalledWith(evt); + }); }); diff --git a/src/useKeyboardEvent/__tests__/ssr.ts b/src/useKeyboardEvent/__tests__/ssr.ts index c612cbced..e4f859d77 100644 --- a/src/useKeyboardEvent/__tests__/ssr.ts +++ b/src/useKeyboardEvent/__tests__/ssr.ts @@ -2,14 +2,14 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useKeyboardEvent } from '../..'; describe('useKeyboardEvent', () => { - it('should be defined', () => { - expect(useKeyboardEvent).toBeDefined(); - }); + it('should be defined', () => { + expect(useKeyboardEvent).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useKeyboardEvent('a', () => {}); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useKeyboardEvent('a', () => {}); + }); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useKeyboardEvent/index.ts b/src/useKeyboardEvent/index.ts index 8af201d4d..09ad053ef 100644 --- a/src/useKeyboardEvent/index.ts +++ b/src/useKeyboardEvent/index.ts @@ -9,26 +9,26 @@ export type KeyboardEventFilter = null | string | boolean | KeyboardEventPredica export type KeyboardEventHandler = (this: T, event: KeyboardEvent) => void; export type UseKeyboardEventOptions = { - /** - * Keyboard event which triggers `callback`. - * @default `keydown` - */ - event?: 'keydown' | 'keypress' | 'keyup'; - /** - * Target element that emits `event`. - * @default window - */ - target?: RefObject | T | null; - /** - * Options passed to the underlying `useEventListener` hook. - */ - eventOptions?: boolean | AddEventListenerOptions; + /** + * Keyboard event which triggers `callback`. + * @default `keydown` + */ + event?: 'keydown' | 'keypress' | 'keyup'; + /** + * Target element that emits `event`. + * @default window + */ + target?: RefObject | T | null; + /** + * Options passed to the underlying `useEventListener` hook. + */ + eventOptions?: boolean | AddEventListenerOptions; }; const createKeyPredicate = (keyFilter: KeyboardEventFilter): KeyboardEventPredicate => { - if (typeof keyFilter === 'function') return keyFilter; - if (typeof keyFilter === 'string') return (ev) => ev.key === keyFilter; - return keyFilter ? yieldTrue : yieldFalse; + if (typeof keyFilter === 'function') return keyFilter; + if (typeof keyFilter === 'string') return (ev) => ev.key === keyFilter; + return keyFilter ? yieldTrue : yieldFalse; }; const WINDOW_OR_NULL = isBrowser ? window : null; @@ -42,23 +42,23 @@ const WINDOW_OR_NULL = isBrowser ? window : null; * @param options Hook options. */ export function useKeyboardEvent( - keyOrPredicate: KeyboardEventFilter, - callback: KeyboardEventHandler, - deps?: DependencyList, - options: UseKeyboardEventOptions = {} + keyOrPredicate: KeyboardEventFilter, + callback: KeyboardEventHandler, + deps?: DependencyList, + options: UseKeyboardEventOptions = {} ): void { - const { event = 'keydown', target = WINDOW_OR_NULL, eventOptions } = options; - const cbRef = useSyncedRef(callback); + const { event = 'keydown', target = WINDOW_OR_NULL, eventOptions } = options; + const cbRef = useSyncedRef(callback); - const handler = useMemo>(() => { - const predicate = createKeyPredicate(keyOrPredicate); + const handler = useMemo>(() => { + const predicate = createKeyPredicate(keyOrPredicate); - return function (this: T, ev) { - if (predicate(ev)) { - cbRef.current.call(this, ev); - } - }; - }, deps); + return function (this: T, ev) { + if (predicate(ev)) { + cbRef.current.call(this, ev); + } + }; + }, deps); - useEventListener(target, event, handler, eventOptions); + useEventListener(target, event, handler, eventOptions); } diff --git a/src/useLifecycleLogger/__docs__/example.stories.tsx b/src/useLifecycleLogger/__docs__/example.stories.tsx index 8a266d2ba..4be52f09a 100644 --- a/src/useLifecycleLogger/__docs__/example.stories.tsx +++ b/src/useLifecycleLogger/__docs__/example.stories.tsx @@ -2,20 +2,20 @@ import * as React from 'react'; import { useRerender, useLifecycleLogger } from '../..'; export function Example() { - const rerender = useRerender(); - const dependency = 'test'; - useLifecycleLogger('Demo', [dependency]); + const rerender = useRerender(); + const dependency = 'test'; + useLifecycleLogger('Demo', [dependency]); - return ( -
-
Check your console for useLifecycleLogger logs
- -
- ); + return ( +
+
Check your console for useLifecycleLogger logs
+ +
+ ); } diff --git a/src/useLifecycleLogger/__docs__/story.mdx b/src/useLifecycleLogger/__docs__/story.mdx index 74b41d880..931c552cb 100644 --- a/src/useLifecycleLogger/__docs__/story.mdx +++ b/src/useLifecycleLogger/__docs__/story.mdx @@ -11,7 +11,7 @@ React lifecycle hook that console logs parameters as component transitions throu #### Example - + ## Reference diff --git a/src/useLifecycleLogger/__tests__/dom.ts b/src/useLifecycleLogger/__tests__/dom.ts index 121dd9391..c92e80613 100644 --- a/src/useLifecycleLogger/__tests__/dom.ts +++ b/src/useLifecycleLogger/__tests__/dom.ts @@ -2,44 +2,44 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useLifecycleLogger } from '../..'; describe('useLifecycleLogger', () => { - let logSpy: jest.SpyInstance; + let logSpy: jest.SpyInstance; - beforeAll(() => { - logSpy = jest.spyOn(console, 'log'); - }); + beforeAll(() => { + logSpy = jest.spyOn(console, 'log'); + }); - afterAll(() => { - logSpy.mockRestore(); - }); + afterAll(() => { + logSpy.mockRestore(); + }); - beforeEach(() => { - logSpy.mockReset(); - }); + beforeEach(() => { + logSpy.mockReset(); + }); - it('should log whole component lifecycle', () => { - const { unmount, rerender } = renderHook( - ({ deps }) => { - useLifecycleLogger('TestComponent', deps); - }, - { initialProps: { deps: [1, 2, 3] } } - ); + it('should log whole component lifecycle', () => { + const { unmount, rerender } = renderHook( + ({ deps }) => { + useLifecycleLogger('TestComponent', deps); + }, + { initialProps: { deps: [1, 2, 3] } } + ); - expect(logSpy).toHaveBeenCalledTimes(1); - expect(logSpy).toHaveBeenCalledWith(`TestComponent mounted`, [1, 2, 3]); + expect(logSpy).toHaveBeenCalledTimes(1); + expect(logSpy).toHaveBeenCalledWith(`TestComponent mounted`, [1, 2, 3]); - rerender({ deps: [3, 2, 1] }); + rerender({ deps: [3, 2, 1] }); - expect(logSpy).toHaveBeenCalledTimes(2); - expect(logSpy).toHaveBeenCalledWith(`TestComponent updated`, [3, 2, 1]); + expect(logSpy).toHaveBeenCalledTimes(2); + expect(logSpy).toHaveBeenCalledWith(`TestComponent updated`, [3, 2, 1]); - rerender({ deps: [1, 5, 6] }); + rerender({ deps: [1, 5, 6] }); - expect(logSpy).toHaveBeenCalledTimes(3); - expect(logSpy).toHaveBeenCalledWith(`TestComponent updated`, [1, 5, 6]); + expect(logSpy).toHaveBeenCalledTimes(3); + expect(logSpy).toHaveBeenCalledWith(`TestComponent updated`, [1, 5, 6]); - unmount(); + unmount(); - expect(logSpy).toHaveBeenCalledTimes(4); - expect(logSpy).toHaveBeenCalledWith(`TestComponent unmounted`); - }); + expect(logSpy).toHaveBeenCalledTimes(4); + expect(logSpy).toHaveBeenCalledWith(`TestComponent unmounted`); + }); }); diff --git a/src/useLifecycleLogger/__tests__/ssr.ts b/src/useLifecycleLogger/__tests__/ssr.ts index 1eee6cf6b..fa2208cac 100644 --- a/src/useLifecycleLogger/__tests__/ssr.ts +++ b/src/useLifecycleLogger/__tests__/ssr.ts @@ -2,14 +2,14 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useLifecycleLogger } from '../..'; describe('useLifecycleLogger', () => { - it('should be defined', () => { - expect(useLifecycleLogger).toBeDefined(); - }); + it('should be defined', () => { + expect(useLifecycleLogger).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useLifecycleLogger('TestComponent'); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useLifecycleLogger('TestComponent'); + }); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useLifecycleLogger/index.ts b/src/useLifecycleLogger/index.ts index 38f8b0539..5081b863e 100644 --- a/src/useLifecycleLogger/index.ts +++ b/src/useLifecycleLogger/index.ts @@ -7,21 +7,21 @@ import { type DependencyList, useEffect, useRef } from 'react'; * @param deps Dependencies list, as for `useEffect` hook */ export function useLifecycleLogger(componentName: string, deps?: DependencyList): void { - const mountedRef = useRef(false); + const mountedRef = useRef(false); - useEffect(() => { - if (mountedRef.current) { - console.log(`${componentName} updated`, deps && [...deps]); - } - }, deps); + useEffect(() => { + if (mountedRef.current) { + console.log(`${componentName} updated`, deps && [...deps]); + } + }, deps); - useEffect(() => { - mountedRef.current = true; - console.log(`${componentName} mounted`, deps && [...deps]); + useEffect(() => { + mountedRef.current = true; + console.log(`${componentName} mounted`, deps && [...deps]); - return () => { - mountedRef.current = false; - console.log(`${componentName} unmounted`); - }; - }, []); + return () => { + mountedRef.current = false; + console.log(`${componentName} unmounted`); + }; + }, []); } diff --git a/src/useList/__docs__/example.stories.tsx b/src/useList/__docs__/example.stories.tsx index 0d914a190..b2810f005 100644 --- a/src/useList/__docs__/example.stories.tsx +++ b/src/useList/__docs__/example.stories.tsx @@ -2,118 +2,118 @@ import * as React from 'react'; import { useList } from '../..'; export function Example() { - const [ - list, - { - set, - push, - updateAt, - insertAt, - update, - updateFirst, - upsert, - sort, - filter, - removeAt, - clear, - reset, - }, - ] = useList([1, 2, 3, 4, 5]); + const [ + list, + { + set, + push, + updateAt, + insertAt, + update, + updateFirst, + upsert, + sort, + filter, + removeAt, + clear, + reset, + }, + ] = useList([1, 2, 3, 4, 5]); - return ( -
- - - -
-
- - - -
-
- - - -
-
- - -
-
- - -
{JSON.stringify(list, null, 2)}
-
- ); + return ( +
+ + + +
+
+ + + +
+
+ + + +
+
+ + +
+
+ + +
{JSON.stringify(list, null, 2)}
+
+ ); } diff --git a/src/useList/__docs__/story.mdx b/src/useList/__docs__/story.mdx index 13dd8229b..4486f81fe 100644 --- a/src/useList/__docs__/story.mdx +++ b/src/useList/__docs__/story.mdx @@ -15,7 +15,7 @@ Manipulating the list directly will not cause a rerender. Instead, use the offer #### Example - + ## Reference diff --git a/src/useList/__tests__/dom.ts b/src/useList/__tests__/dom.ts index f89a0865a..c9bb032fd 100644 --- a/src/useList/__tests__/dom.ts +++ b/src/useList/__tests__/dom.ts @@ -2,350 +2,350 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { useList } from '../..'; describe('useList', () => { - it('should be defined', () => { - expect(useList).toBeDefined(); - }); + it('should be defined', () => { + expect(useList).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useList([])); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useList([])); + expect(result.error).toBeUndefined(); + }); - it('should accept an initial list', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - expect(result.current[0]).toEqual([0, 1, 2]); - }); + it('should accept an initial list', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + expect(result.current[0]).toEqual([0, 1, 2]); + }); - it('should return same actions object on every render', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const actions = result.current[1]; + it('should return same actions object on every render', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const actions = result.current[1]; - act(() => { - actions.set([3, 4, 5]); - }); + act(() => { + actions.set([3, 4, 5]); + }); - expect(result.current[1]).toEqual(actions); - }); + expect(result.current[1]).toEqual(actions); + }); - describe('set', () => { - it('should replace the current list', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const { set } = result.current[1]; + describe('set', () => { + it('should replace the current list', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const { set } = result.current[1]; - act(() => { - set([3, 4, 5]); - }); + act(() => { + set([3, 4, 5]); + }); - expect(result.current[0]).toEqual([3, 4, 5]); - }); + expect(result.current[0]).toEqual([3, 4, 5]); + }); - it('should replace the current list with empty list', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const { set } = result.current[1]; + it('should replace the current list with empty list', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const { set } = result.current[1]; - act(() => { - set([]); - }); + act(() => { + set([]); + }); - expect(result.current[0]).toEqual([]); - }); - - it('should functionally replace the current list', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const { set } = result.current[1]; - - act(() => { - set((current) => [...current, 3]); - }); - - expect(result.current[0]).toEqual([0, 1, 2, 3]); - }); - }); - - describe('push', () => { - it('should push a new item to the list', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const { push } = result.current[1]; - - act(() => { - push(3); - }); - - expect(result.current[0]).toEqual([0, 1, 2, 3]); - }); - - it('should push multiple items to the list', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const { push } = result.current[1]; - - act(() => { - push(3, 4, 5); - }); - - expect(result.current[0]).toEqual([0, 1, 2, 3, 4, 5]); - }); - }); - - describe('updateAt', () => { - it('should update item at given position', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const { updateAt } = result.current[1]; - - act(() => { - updateAt(1, 0); - }); - - expect(result.current[0]).toEqual([0, 0, 2]); - }); - - it('should update item at position that is out of of bounds', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const { updateAt } = result.current[1]; - - act(() => { - updateAt(4, 0); - }); + expect(result.current[0]).toEqual([]); + }); + + it('should functionally replace the current list', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const { set } = result.current[1]; + + act(() => { + set((current) => [...current, 3]); + }); + + expect(result.current[0]).toEqual([0, 1, 2, 3]); + }); + }); + + describe('push', () => { + it('should push a new item to the list', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const { push } = result.current[1]; + + act(() => { + push(3); + }); + + expect(result.current[0]).toEqual([0, 1, 2, 3]); + }); + + it('should push multiple items to the list', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const { push } = result.current[1]; + + act(() => { + push(3, 4, 5); + }); + + expect(result.current[0]).toEqual([0, 1, 2, 3, 4, 5]); + }); + }); + + describe('updateAt', () => { + it('should update item at given position', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const { updateAt } = result.current[1]; + + act(() => { + updateAt(1, 0); + }); + + expect(result.current[0]).toEqual([0, 0, 2]); + }); + + it('should update item at position that is out of of bounds', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const { updateAt } = result.current[1]; + + act(() => { + updateAt(4, 0); + }); - expect(result.current[0]).toEqual([0, 1, 2, undefined, 0]); - }); - }); + expect(result.current[0]).toEqual([0, 1, 2, undefined, 0]); + }); + }); - describe('insertAt', () => { - it('should insert item into given position in the list', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const { insertAt } = result.current[1]; + describe('insertAt', () => { + it('should insert item into given position in the list', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const { insertAt } = result.current[1]; - act(() => { - insertAt(1, 0); - }); + act(() => { + insertAt(1, 0); + }); - expect(result.current[0]).toEqual([0, 0, 1, 2]); - }); + expect(result.current[0]).toEqual([0, 0, 1, 2]); + }); - it('should insert item into position that is out of bounds', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const { insertAt } = result.current[1]; + it('should insert item into position that is out of bounds', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const { insertAt } = result.current[1]; - act(() => { - insertAt(4, 0); - }); + act(() => { + insertAt(4, 0); + }); - expect(result.current[0]).toEqual([0, 1, 2, undefined, 0]); - }); - }); + expect(result.current[0]).toEqual([0, 1, 2, undefined, 0]); + }); + }); - describe('update', () => { - it('should update all items that match given predicate', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const { update } = result.current[1]; + describe('update', () => { + it('should update all items that match given predicate', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const { update } = result.current[1]; - act(() => { - update((iteratedItem: number) => iteratedItem > 0, 0); - }); + act(() => { + update((iteratedItem: number) => iteratedItem > 0, 0); + }); - expect(result.current[0]).toEqual([0, 0, 0]); - }); + expect(result.current[0]).toEqual([0, 0, 0]); + }); - it('should pass update predicate the iterated element and the replacement', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const { update } = result.current[1]; - const predicate = jest.fn((_iteratedItem, _newElement) => false); - - act(() => { - update(predicate, 0); - }); - - expect(numberOfMockFunctionCalls(predicate)).toEqual(3); - expect(mockFunctionCallArgument(predicate, 0, 0)).toBe(0); - expect(mockFunctionCallArgument(predicate, 0, 1)).toBe(0); - }); - - it('should not update any items if none match given predicate', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const { update } = result.current[1]; - - act(() => { - update((iteratedItem: number) => iteratedItem > 3, 0); - }); - - expect(result.current[0]).toEqual([0, 1, 2]); - }); - }); - - describe('updateFirst', () => { - it('should update the first item matching the given predicate', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const { updateFirst } = result.current[1]; - - act(() => { - updateFirst((iteratedItem: number) => iteratedItem > 0, 0); - }); - - expect(result.current[0]).toEqual([0, 0, 2]); - }); - - it('should not update any items if none match given predicate', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const { updateFirst } = result.current[1]; - - act(() => { - updateFirst((iteratedItem: number) => iteratedItem > 3, 0); - }); - - expect(result.current[0]).toEqual([0, 1, 2]); - }); - }); - - describe('upsert', () => { - it('should update the first item matching the given predicate', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const { upsert } = result.current[1]; - - act(() => { - upsert((iteratedItem: number) => iteratedItem > 0, 0); - }); - - expect(result.current[0]).toEqual([0, 0, 2]); - }); - - it('should push given item to list, if no item matches the predicate', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const { upsert } = result.current[1]; - - act(() => { - upsert((iteratedItem: number) => iteratedItem > 3, 0); - }); - - expect(result.current[0]).toEqual([0, 1, 2, 0]); - }); - - it('should pass predicate the iterated element and the new element', () => { - const { result } = renderHook(() => useList([0, 1, 2])); - const { upsert } = result.current[1]; - const predicate = jest.fn((_iteratedItem, _newElement) => false); - - act(() => { - upsert(predicate, 0); - }); - - expect(numberOfMockFunctionCalls(predicate)).toEqual(3); - expect(mockFunctionCallArgument(predicate, 0, 0)).toBe(0); - expect(mockFunctionCallArgument(predicate, 0, 1)).toBe(0); - }); - }); - - describe('sort', () => { - it('should sort list with given sorting function', () => { - const { result } = renderHook(() => useList([1, 0, 2])); - const { sort } = result.current[1]; - - act(() => { - sort((a, b) => b - a); - }); - - expect(result.current[0]).toEqual([2, 1, 0]); - }); - - it('should use default sorting if sort is called without arguments', () => { - const { result } = renderHook(() => useList([1, 0, 2])); - const { sort } = result.current[1]; - - act(() => { - sort(); - }); - - expect(result.current[0]).toEqual([0, 1, 2]); - }); - }); - - describe('filter', () => { - it('should filter list with given filter function', () => { - const { result } = renderHook(() => useList([1, 0, 2])); - const { filter } = result.current[1]; - - act(() => { - filter((a) => a > 0); - }); - - expect(result.current[0]).toEqual([1, 2]); - }); - - it('should pass element, its index and iterated list to filter function', () => { - const { result } = renderHook(() => useList([1, 0, 2])); - const { filter } = result.current[1]; - const filterFunction = jest.fn((_element, _index, _list) => false); - - act(() => { - filter(filterFunction); - }); - - expect(numberOfMockFunctionCalls(filterFunction)).toEqual(3); - expect(mockFunctionCallArgument(filterFunction, 0, 0)).toBe(1); - expect(mockFunctionCallArgument(filterFunction, 0, 1)).toBe(0); - expect(mockFunctionCallArgument(filterFunction, 0, 2)).toEqual([1, 0, 2]); - }); - }); - - describe('removeAt', () => { - it('should remove item from given index', () => { - const { result } = renderHook(() => useList([1, 0, 2])); - const { removeAt } = result.current[1]; - - act(() => { - removeAt(1); - }); - - expect(result.current[0]).toEqual([1, 2]); - }); - - it('should not remove items if given index is out of bounds', () => { - const { result } = renderHook(() => useList([1, 0, 2])); - const { removeAt } = result.current[1]; - - act(() => { - removeAt(6); - }); - - expect(result.current[0]).toEqual([1, 0, 2]); - }); - }); - - describe('clear', () => { - it('should clear the list', () => { - const { result } = renderHook(() => useList([1, 0, 2])); - const { clear } = result.current[1]; - - act(() => { - clear(); - }); - - expect(result.current[0]).toEqual([]); - }); - }); - - describe('reset', () => { - it('should reset the list to initial value', () => { - const { result } = renderHook(() => useList([1, 0, 2])); - const { reset, set } = result.current[1]; - - act(() => { - set([1, 1, 1]); - reset(); - }); - - expect(result.current[0]).toEqual([1, 0, 2]); - }); - }); + it('should pass update predicate the iterated element and the replacement', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const { update } = result.current[1]; + const predicate = jest.fn((_iteratedItem, _newElement) => false); + + act(() => { + update(predicate, 0); + }); + + expect(numberOfMockFunctionCalls(predicate)).toEqual(3); + expect(mockFunctionCallArgument(predicate, 0, 0)).toBe(0); + expect(mockFunctionCallArgument(predicate, 0, 1)).toBe(0); + }); + + it('should not update any items if none match given predicate', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const { update } = result.current[1]; + + act(() => { + update((iteratedItem: number) => iteratedItem > 3, 0); + }); + + expect(result.current[0]).toEqual([0, 1, 2]); + }); + }); + + describe('updateFirst', () => { + it('should update the first item matching the given predicate', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const { updateFirst } = result.current[1]; + + act(() => { + updateFirst((iteratedItem: number) => iteratedItem > 0, 0); + }); + + expect(result.current[0]).toEqual([0, 0, 2]); + }); + + it('should not update any items if none match given predicate', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const { updateFirst } = result.current[1]; + + act(() => { + updateFirst((iteratedItem: number) => iteratedItem > 3, 0); + }); + + expect(result.current[0]).toEqual([0, 1, 2]); + }); + }); + + describe('upsert', () => { + it('should update the first item matching the given predicate', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const { upsert } = result.current[1]; + + act(() => { + upsert((iteratedItem: number) => iteratedItem > 0, 0); + }); + + expect(result.current[0]).toEqual([0, 0, 2]); + }); + + it('should push given item to list, if no item matches the predicate', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const { upsert } = result.current[1]; + + act(() => { + upsert((iteratedItem: number) => iteratedItem > 3, 0); + }); + + expect(result.current[0]).toEqual([0, 1, 2, 0]); + }); + + it('should pass predicate the iterated element and the new element', () => { + const { result } = renderHook(() => useList([0, 1, 2])); + const { upsert } = result.current[1]; + const predicate = jest.fn((_iteratedItem, _newElement) => false); + + act(() => { + upsert(predicate, 0); + }); + + expect(numberOfMockFunctionCalls(predicate)).toEqual(3); + expect(mockFunctionCallArgument(predicate, 0, 0)).toBe(0); + expect(mockFunctionCallArgument(predicate, 0, 1)).toBe(0); + }); + }); + + describe('sort', () => { + it('should sort list with given sorting function', () => { + const { result } = renderHook(() => useList([1, 0, 2])); + const { sort } = result.current[1]; + + act(() => { + sort((a, b) => b - a); + }); + + expect(result.current[0]).toEqual([2, 1, 0]); + }); + + it('should use default sorting if sort is called without arguments', () => { + const { result } = renderHook(() => useList([1, 0, 2])); + const { sort } = result.current[1]; + + act(() => { + sort(); + }); + + expect(result.current[0]).toEqual([0, 1, 2]); + }); + }); + + describe('filter', () => { + it('should filter list with given filter function', () => { + const { result } = renderHook(() => useList([1, 0, 2])); + const { filter } = result.current[1]; + + act(() => { + filter((a) => a > 0); + }); + + expect(result.current[0]).toEqual([1, 2]); + }); + + it('should pass element, its index and iterated list to filter function', () => { + const { result } = renderHook(() => useList([1, 0, 2])); + const { filter } = result.current[1]; + const filterFunction = jest.fn((_element, _index, _list) => false); + + act(() => { + filter(filterFunction); + }); + + expect(numberOfMockFunctionCalls(filterFunction)).toEqual(3); + expect(mockFunctionCallArgument(filterFunction, 0, 0)).toBe(1); + expect(mockFunctionCallArgument(filterFunction, 0, 1)).toBe(0); + expect(mockFunctionCallArgument(filterFunction, 0, 2)).toEqual([1, 0, 2]); + }); + }); + + describe('removeAt', () => { + it('should remove item from given index', () => { + const { result } = renderHook(() => useList([1, 0, 2])); + const { removeAt } = result.current[1]; + + act(() => { + removeAt(1); + }); + + expect(result.current[0]).toEqual([1, 2]); + }); + + it('should not remove items if given index is out of bounds', () => { + const { result } = renderHook(() => useList([1, 0, 2])); + const { removeAt } = result.current[1]; + + act(() => { + removeAt(6); + }); + + expect(result.current[0]).toEqual([1, 0, 2]); + }); + }); + + describe('clear', () => { + it('should clear the list', () => { + const { result } = renderHook(() => useList([1, 0, 2])); + const { clear } = result.current[1]; + + act(() => { + clear(); + }); + + expect(result.current[0]).toEqual([]); + }); + }); + + describe('reset', () => { + it('should reset the list to initial value', () => { + const { result } = renderHook(() => useList([1, 0, 2])); + const { reset, set } = result.current[1]; + + act(() => { + set([1, 1, 1]); + reset(); + }); + + expect(result.current[0]).toEqual([1, 0, 2]); + }); + }); }); function numberOfMockFunctionCalls(mockFunction: jest.Mock) { - return mockFunction.mock.calls.length; + return mockFunction.mock.calls.length; } function mockFunctionCallArgument( - mockFunction: jest.Mock, - callIndex: number, - argumentIndex: number + mockFunction: jest.Mock, + callIndex: number, + argumentIndex: number ) { - return mockFunction.mock.calls[callIndex][argumentIndex]; + return mockFunction.mock.calls[callIndex][argumentIndex]; } diff --git a/src/useList/__tests__/ssr.ts b/src/useList/__tests__/ssr.ts index f125a630d..1bc0b5a3a 100644 --- a/src/useList/__tests__/ssr.ts +++ b/src/useList/__tests__/ssr.ts @@ -2,12 +2,12 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useList } from '../..'; describe('useList', () => { - it('should be defined', () => { - expect(useList).toBeDefined(); - }); + it('should be defined', () => { + expect(useList).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useList([1, 0, 2])); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useList([1, 0, 2])); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useList/index.ts b/src/useList/index.ts index 7a13ce9c0..367f22623 100644 --- a/src/useList/index.ts +++ b/src/useList/index.ts @@ -4,169 +4,169 @@ import { useRerender } from '../useRerender'; import { useSyncedRef } from '../useSyncedRef'; export type ListActions = { - /** - * Replaces the current list. - */ - set: (newList: SetStateAction) => void; - - /** - * Adds an item or items to the end of the list. - */ - push: (...items: T[]) => void; - - /** - * Replaces the item at the given index of the list. If the given index is out of bounds, empty - * elements are appended to the list until the given item can be set to the given index. - */ - updateAt: (index: number, newItem: T) => void; - - /** - * Inserts an item at the given index of the list. All items following the given index are shifted - * one position. If the given index is out of bounds, empty elements are appended to the list until - * the given item can be set to the given index. - */ - insertAt: (index: number, item: T) => void; - - /** - * Replaces all items of the list that match the given predicate with the given item. - */ - update: (predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) => void; - - /** - * Replaces the first item of the list that matches the given predicate with the given item. - */ - updateFirst: (predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) => void; - - /** - * Replaces the first item of the list that matches the given predicate with the given item. If - * none of the items match the predicate, the given item is pushed to the list. - */ - upsert: (predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) => void; - - /** - * Sorts the list with the given sorting function. If no sorting function is given, the default - * Array.prototype.sort() sorting is used. - */ - sort: (compareFn?: (a: T, b: T) => number) => void; - - /** - * Filters the list with the given filter function. - */ - // We're allowing the type of thisArg to be any, because we are following the Array.prototype.filter API. - - filter: (callbackFn: (value: T, index?: number, array?: T[]) => boolean, thisArg?: any) => void; - - /** - * Removes the item at the given index of the list. All items following the given index will be - * shifted. If the given index is out of the bounds of the list, the list will not be modified, - * but a rerender will occur. - */ - removeAt: (index: number) => void; - - /** - * Deletes all items of the list. - */ - clear: () => void; - - /** - * Replaces the current list with the initial list given to this hook. - */ - reset: () => void; + /** + * Replaces the current list. + */ + set: (newList: SetStateAction) => void; + + /** + * Adds an item or items to the end of the list. + */ + push: (...items: T[]) => void; + + /** + * Replaces the item at the given index of the list. If the given index is out of bounds, empty + * elements are appended to the list until the given item can be set to the given index. + */ + updateAt: (index: number, newItem: T) => void; + + /** + * Inserts an item at the given index of the list. All items following the given index are shifted + * one position. If the given index is out of bounds, empty elements are appended to the list until + * the given item can be set to the given index. + */ + insertAt: (index: number, item: T) => void; + + /** + * Replaces all items of the list that match the given predicate with the given item. + */ + update: (predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) => void; + + /** + * Replaces the first item of the list that matches the given predicate with the given item. + */ + updateFirst: (predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) => void; + + /** + * Replaces the first item of the list that matches the given predicate with the given item. If + * none of the items match the predicate, the given item is pushed to the list. + */ + upsert: (predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) => void; + + /** + * Sorts the list with the given sorting function. If no sorting function is given, the default + * Array.prototype.sort() sorting is used. + */ + sort: (compareFn?: (a: T, b: T) => number) => void; + + /** + * Filters the list with the given filter function. + */ + // We're allowing the type of thisArg to be any, because we are following the Array.prototype.filter API. + + filter: (callbackFn: (value: T, index?: number, array?: T[]) => boolean, thisArg?: any) => void; + + /** + * Removes the item at the given index of the list. All items following the given index will be + * shifted. If the given index is out of the bounds of the list, the list will not be modified, + * but a rerender will occur. + */ + removeAt: (index: number) => void; + + /** + * Deletes all items of the list. + */ + clear: () => void; + + /** + * Replaces the current list with the initial list given to this hook. + */ + reset: () => void; }; export function useList(initialList: InitialState): [T[], ListActions] { - const initial = useSyncedRef(initialList); - const list = useRef(resolveHookState(initial.current)); - const rerender = useRerender(); - - const actions = useMemo( - () => ({ - set(newList: SetStateAction) { - list.current = resolveHookState(newList, list.current); - rerender(); - }, - - push(...items: T[]) { - actions.set((currentList: T[]) => [...currentList, ...items]); - }, - - updateAt(index: number, newItem: T) { - actions.set((currentList: T[]) => { - const listCopy = [...currentList]; - listCopy[index] = newItem; - return listCopy; - }); - }, - - insertAt(index: number, newItem: T) { - actions.set((currentList: T[]) => { - const listCopy = [...currentList]; - - if (index >= listCopy.length) { - listCopy[index] = newItem; - } else { - listCopy.splice(index, 0, newItem); - } - - return listCopy; - }); - }, - - update(predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) { - actions.set((currentList: T[]) => - currentList.map((item: T) => (predicate(item, newItem) ? newItem : item)) - ); - }, - - updateFirst(predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) { - const indexOfMatch = list.current.findIndex((item: T) => predicate(item, newItem)); - - const NO_MATCH = -1; - if (indexOfMatch > NO_MATCH) { - actions.updateAt(indexOfMatch, newItem); - } - }, - - upsert(predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) { - const indexOfMatch = list.current.findIndex((item: T) => predicate(item, newItem)); - - const NO_MATCH = -1; - if (indexOfMatch > NO_MATCH) { - actions.updateAt(indexOfMatch, newItem); - } else { - actions.push(newItem); - } - }, - - sort(compareFn?: (a: T, b: T) => number) { - actions.set((currentList: T[]) => [...currentList].sort(compareFn)); - }, - - filter(callbackFn: (value: T, index: number, array: T[]) => boolean, thisArg?: never) { - actions.set((currentList: T[]) => [...currentList].filter(callbackFn, thisArg)); - }, - - removeAt(index: number) { - actions.set((currentList: T[]) => { - const listCopy = [...currentList]; - if (index < listCopy.length) { - listCopy.splice(index, 1); - } - - return listCopy; - }); - }, - - clear() { - actions.set([]); - }, - - reset() { - actions.set([...resolveHookState(initial.current)]); - }, - }), - [initial, rerender] - ); - - return [list.current, actions]; + const initial = useSyncedRef(initialList); + const list = useRef(resolveHookState(initial.current)); + const rerender = useRerender(); + + const actions = useMemo( + () => ({ + set(newList: SetStateAction) { + list.current = resolveHookState(newList, list.current); + rerender(); + }, + + push(...items: T[]) { + actions.set((currentList: T[]) => [...currentList, ...items]); + }, + + updateAt(index: number, newItem: T) { + actions.set((currentList: T[]) => { + const listCopy = [...currentList]; + listCopy[index] = newItem; + return listCopy; + }); + }, + + insertAt(index: number, newItem: T) { + actions.set((currentList: T[]) => { + const listCopy = [...currentList]; + + if (index >= listCopy.length) { + listCopy[index] = newItem; + } else { + listCopy.splice(index, 0, newItem); + } + + return listCopy; + }); + }, + + update(predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) { + actions.set((currentList: T[]) => + currentList.map((item: T) => (predicate(item, newItem) ? newItem : item)) + ); + }, + + updateFirst(predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) { + const indexOfMatch = list.current.findIndex((item: T) => predicate(item, newItem)); + + const NO_MATCH = -1; + if (indexOfMatch > NO_MATCH) { + actions.updateAt(indexOfMatch, newItem); + } + }, + + upsert(predicate: (iteratedItem: T, newItem: T) => boolean, newItem: T) { + const indexOfMatch = list.current.findIndex((item: T) => predicate(item, newItem)); + + const NO_MATCH = -1; + if (indexOfMatch > NO_MATCH) { + actions.updateAt(indexOfMatch, newItem); + } else { + actions.push(newItem); + } + }, + + sort(compareFn?: (a: T, b: T) => number) { + actions.set((currentList: T[]) => [...currentList].sort(compareFn)); + }, + + filter(callbackFn: (value: T, index: number, array: T[]) => boolean, thisArg?: never) { + actions.set((currentList: T[]) => [...currentList].filter(callbackFn, thisArg)); + }, + + removeAt(index: number) { + actions.set((currentList: T[]) => { + const listCopy = [...currentList]; + if (index < listCopy.length) { + listCopy.splice(index, 1); + } + + return listCopy; + }); + }, + + clear() { + actions.set([]); + }, + + reset() { + actions.set([...resolveHookState(initial.current)]); + }, + }), + [initial, rerender] + ); + + return [list.current, actions]; } diff --git a/src/useLocalStorageValue/__docs__/example.stories.tsx b/src/useLocalStorageValue/__docs__/example.stories.tsx index 4c22046b8..28420e35c 100644 --- a/src/useLocalStorageValue/__docs__/example.stories.tsx +++ b/src/useLocalStorageValue/__docs__/example.stories.tsx @@ -2,41 +2,41 @@ import React from 'react'; import { useLocalStorageValue } from '../..'; type ExampleProps = { - /** - * Default value to return in case key not presented in LocalStorage. - */ - defaultValue: string; - /** - * LocalStorage key to manage. - */ - key: string; + /** + * Default value to return in case key not presented in LocalStorage. + */ + defaultValue: string; + /** + * LocalStorage key to manage. + */ + key: string; }; export function Example({ - key = 'react-hookz-ls-test', - defaultValue = '@react-hookz is awesome', + key = 'react-hookz-ls-test', + defaultValue = '@react-hookz is awesome', }: ExampleProps) { - const lsVal = useLocalStorageValue(key, { - defaultValue, - }); + const lsVal = useLocalStorageValue(key, { + defaultValue, + }); - return ( -
-
- Below input value will persist between page reloads and even browser restart as its value is - stored in LocalStorage. -
-
- { - lsVal.set(ev.currentTarget.value); - }} - /> - -
- ); + return ( +
+
+ Below input value will persist between page reloads and even browser restart as its value is + stored in LocalStorage. +
+
+ { + lsVal.set(ev.currentTarget.value); + }} + /> + +
+ ); } diff --git a/src/useLocalStorageValue/__docs__/story.mdx b/src/useLocalStorageValue/__docs__/story.mdx index 0a01be043..5010ce089 100644 --- a/src/useLocalStorageValue/__docs__/story.mdx +++ b/src/useLocalStorageValue/__docs__/story.mdx @@ -28,11 +28,11 @@ Manages a single LocalStorage key. #### Example - -
- It is also synchronised between hooks on the same page -
- + +
+ It is also synchronised between hooks on the same page +
+
@@ -41,12 +41,12 @@ Manages a single LocalStorage key. ```ts function useLocalStorageValue< - Type, - Default extends Type = Type, - Initialize extends boolean | undefined = boolean | undefined + Type, + Default extends Type = Type, + Initialize extends boolean | undefined = boolean | undefined >( - key: string, - options?: UseStorageValueOptions + key: string, + options?: UseStorageValueOptions ): UseStorageValueResult; ``` diff --git a/src/useLocalStorageValue/__tests__/dom.ts b/src/useLocalStorageValue/__tests__/dom.ts index e9d64afae..e3c4120f2 100644 --- a/src/useLocalStorageValue/__tests__/dom.ts +++ b/src/useLocalStorageValue/__tests__/dom.ts @@ -2,14 +2,14 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useLocalStorageValue } from '../..'; describe('useLocalStorageValue', () => { - it('should be defined', () => { - expect(useLocalStorageValue).toBeDefined(); - }); + it('should be defined', () => { + expect(useLocalStorageValue).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useLocalStorageValue('foo'); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useLocalStorageValue('foo'); + }); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useLocalStorageValue/__tests__/ssr.ts b/src/useLocalStorageValue/__tests__/ssr.ts index 6e280aa7d..c42cd9698 100644 --- a/src/useLocalStorageValue/__tests__/ssr.ts +++ b/src/useLocalStorageValue/__tests__/ssr.ts @@ -2,14 +2,14 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useLocalStorageValue } from '../..'; describe('useLocalStorageValue', () => { - it('should be defined', () => { - expect(useLocalStorageValue).toBeDefined(); - }); + it('should be defined', () => { + expect(useLocalStorageValue).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useLocalStorageValue('foo'); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useLocalStorageValue('foo'); + }); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useLocalStorageValue/index.ts b/src/useLocalStorageValue/index.ts index 3e3d6780c..dd194cdaa 100644 --- a/src/useLocalStorageValue/index.ts +++ b/src/useLocalStorageValue/index.ts @@ -1,47 +1,47 @@ import { - useStorageValue, - type UseStorageValueOptions, - type UseStorageValueResult, + useStorageValue, + type UseStorageValueOptions, + type UseStorageValueResult, } from '../useStorageValue'; import { isBrowser, noop } from '../util/const'; let IS_LOCAL_STORAGE_AVAILABLE: boolean; try { - IS_LOCAL_STORAGE_AVAILABLE = isBrowser && Boolean(window.localStorage); + IS_LOCAL_STORAGE_AVAILABLE = isBrowser && Boolean(window.localStorage); } catch { - // No need to test this flag leads to noop behaviour - /* istanbul ignore next */ - IS_LOCAL_STORAGE_AVAILABLE = false; + // No need to test this flag leads to noop behaviour + /* istanbul ignore next */ + IS_LOCAL_STORAGE_AVAILABLE = false; } type UseLocalStorageValue = < - Type, - Default extends Type = Type, - Initialize extends boolean | undefined = boolean | undefined + Type, + Default extends Type = Type, + Initialize extends boolean | undefined = boolean | undefined >( - key: string, - options?: UseStorageValueOptions + key: string, + options?: UseStorageValueOptions ) => UseStorageValueResult; /** * Manages a single localStorage key. */ export const useLocalStorageValue: UseLocalStorageValue = IS_LOCAL_STORAGE_AVAILABLE - ? (key, options) => { - return useStorageValue(localStorage, key, options); - } - : < - Type, - Default extends Type = Type, - Initialize extends boolean | undefined = boolean | undefined - >( - _key: string, - _options?: UseStorageValueOptions - ): UseStorageValueResult => { - if (isBrowser && process.env.NODE_ENV === 'development') { - console.warn('LocalStorage is not available in this environment'); - } + ? (key, options) => { + return useStorageValue(localStorage, key, options); + } + : < + Type, + Default extends Type = Type, + Initialize extends boolean | undefined = boolean | undefined + >( + _key: string, + _options?: UseStorageValueOptions + ): UseStorageValueResult => { + if (isBrowser && process.env.NODE_ENV === 'development') { + console.warn('LocalStorage is not available in this environment'); + } - return { value: undefined as Type, set: noop, remove: noop, fetch: noop }; - }; + return { value: undefined as Type, set: noop, remove: noop, fetch: noop }; + }; diff --git a/src/useMap/__docs__/example.stories.tsx b/src/useMap/__docs__/example.stories.tsx index bf5b47560..830f61750 100644 --- a/src/useMap/__docs__/example.stories.tsx +++ b/src/useMap/__docs__/example.stories.tsx @@ -3,30 +3,30 @@ import * as React from 'react'; import { useMap } from '../..'; export function Example() { - const map = useMap([['@react-hooks', 'is awesome']]); + const map = useMap([['@react-hooks', 'is awesome']]); - return ( -
- - - - -
-
{JSON.stringify([...map], null, 2)}
-
- ); + return ( +
+ + + + +
+
{JSON.stringify([...map], null, 2)}
+
+ ); } diff --git a/src/useMap/__docs__/story.mdx b/src/useMap/__docs__/story.mdx index 1ea7f6e8f..a279f41ac 100644 --- a/src/useMap/__docs__/story.mdx +++ b/src/useMap/__docs__/story.mdx @@ -17,7 +17,7 @@ Tracks the state of a `Map`. #### Example - + ## Reference diff --git a/src/useMap/__tests__/dom.ts b/src/useMap/__tests__/dom.ts index cfa8dda19..adf1bee24 100644 --- a/src/useMap/__tests__/dom.ts +++ b/src/useMap/__tests__/dom.ts @@ -2,78 +2,78 @@ import { renderHook, act } from '@testing-library/react-hooks/dom'; import { useMap } from '../..'; describe('useMap', () => { - it('should be defined', () => { - expect(useMap).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useMap()); - expect(result.error).toBeUndefined(); - }); - - it('should return a Map instance with altered add, clear and delete methods', () => { - const { result } = renderHook(() => useMap()); - expect(result.current).toBeInstanceOf(Map); - expect(result.current.set).not.toBe(Map.prototype.set); - expect(result.current.clear).not.toBe(Map.prototype.clear); - expect(result.current.delete).not.toBe(Map.prototype.delete); - }); - - it('should accept initial values', () => { - const { result } = renderHook(() => - useMap([ - ['foo', 1], - ['bar', 2], - ['baz', 3], - ]) - ); - expect(result.current.get('foo')).toBe(1); - expect(result.current.get('bar')).toBe(2); - expect(result.current.get('baz')).toBe(3); - expect(result.current.size).toBe(3); - }); - - it('`set` should invoke original method and rerender component', () => { - const spy = jest.spyOn(Map.prototype, 'set'); - let i = 0; - const { result } = renderHook(() => [++i, useMap()] as const); - - act(() => { - expect(result.current[1].set('foo', 'bar')).toBe(result.current[1]); - expect(spy).toHaveBeenCalledWith('foo', 'bar'); - }); - - expect(result.current[0]).toBe(2); - - spy.mockRestore(); - }); - - it('`clear` should invoke original method and rerender component', () => { - const spy = jest.spyOn(Map.prototype, 'clear'); - let i = 0; - const { result } = renderHook(() => [++i, useMap()] as const); - - act(() => { - result.current[1].clear(); - }); - - expect(result.current[0]).toBe(2); - - spy.mockRestore(); - }); - - it('`delete` should invoke original method and rerender component', () => { - const spy = jest.spyOn(Map.prototype, 'delete'); - let i = 0; - const { result } = renderHook(() => [++i, useMap([['foo', 1]])] as const); - - act(() => { - expect(result.current[1].delete('foo')).toBe(true); - expect(spy).toHaveBeenCalledWith('foo'); - }); - - expect(result.current[0]).toBe(2); - - spy.mockRestore(); - }); + it('should be defined', () => { + expect(useMap).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useMap()); + expect(result.error).toBeUndefined(); + }); + + it('should return a Map instance with altered add, clear and delete methods', () => { + const { result } = renderHook(() => useMap()); + expect(result.current).toBeInstanceOf(Map); + expect(result.current.set).not.toBe(Map.prototype.set); + expect(result.current.clear).not.toBe(Map.prototype.clear); + expect(result.current.delete).not.toBe(Map.prototype.delete); + }); + + it('should accept initial values', () => { + const { result } = renderHook(() => + useMap([ + ['foo', 1], + ['bar', 2], + ['baz', 3], + ]) + ); + expect(result.current.get('foo')).toBe(1); + expect(result.current.get('bar')).toBe(2); + expect(result.current.get('baz')).toBe(3); + expect(result.current.size).toBe(3); + }); + + it('`set` should invoke original method and rerender component', () => { + const spy = jest.spyOn(Map.prototype, 'set'); + let i = 0; + const { result } = renderHook(() => [++i, useMap()] as const); + + act(() => { + expect(result.current[1].set('foo', 'bar')).toBe(result.current[1]); + expect(spy).toHaveBeenCalledWith('foo', 'bar'); + }); + + expect(result.current[0]).toBe(2); + + spy.mockRestore(); + }); + + it('`clear` should invoke original method and rerender component', () => { + const spy = jest.spyOn(Map.prototype, 'clear'); + let i = 0; + const { result } = renderHook(() => [++i, useMap()] as const); + + act(() => { + result.current[1].clear(); + }); + + expect(result.current[0]).toBe(2); + + spy.mockRestore(); + }); + + it('`delete` should invoke original method and rerender component', () => { + const spy = jest.spyOn(Map.prototype, 'delete'); + let i = 0; + const { result } = renderHook(() => [++i, useMap([['foo', 1]])] as const); + + act(() => { + expect(result.current[1].delete('foo')).toBe(true); + expect(spy).toHaveBeenCalledWith('foo'); + }); + + expect(result.current[0]).toBe(2); + + spy.mockRestore(); + }); }); diff --git a/src/useMap/__tests__/ssr.ts b/src/useMap/__tests__/ssr.ts index b16b8427b..7bb8d358d 100644 --- a/src/useMap/__tests__/ssr.ts +++ b/src/useMap/__tests__/ssr.ts @@ -2,78 +2,78 @@ import { renderHook, act } from '@testing-library/react-hooks/server'; import { useMap } from '../..'; describe('useMap', () => { - it('should be defined', () => { - expect(useMap).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useMap()); - expect(result.error).toBeUndefined(); - }); - - it('should return a Map instance with altered set, clear and delete methods', () => { - const { result } = renderHook(() => useMap()); - expect(result.current).toBeInstanceOf(Map); - expect(result.current.set).not.toBe(Map.prototype.set); - expect(result.current.clear).not.toBe(Map.prototype.clear); - expect(result.current.delete).not.toBe(Map.prototype.delete); - }); - - it('should accept initial values', () => { - const { result } = renderHook(() => - useMap([ - ['foo', 1], - ['bar', 2], - ['baz', 3], - ]) - ); - expect(result.current.get('foo')).toBe(1); - expect(result.current.get('bar')).toBe(2); - expect(result.current.get('baz')).toBe(3); - expect(result.current.size).toBe(3); - }); - - it('`set` should invoke original method and not rerender component', () => { - const spy = jest.spyOn(Map.prototype, 'set'); - let i = 0; - const { result } = renderHook(() => [++i, useMap()] as const); - - act(() => { - expect(result.current[1].set('foo', 'bar')).toBe(result.current[1]); - expect(spy).toHaveBeenCalledWith('foo', 'bar'); - }); - - expect(result.current[0]).toBe(1); - - spy.mockRestore(); - }); - - it('`clear` should invoke original method and not rerender component', () => { - const spy = jest.spyOn(Map.prototype, 'clear'); - let i = 0; - const { result } = renderHook(() => [++i, useMap()] as const); - - act(() => { - result.current[1].clear(); - }); - - expect(result.current[0]).toBe(1); - - spy.mockRestore(); - }); - - it('`delete` should invoke original method and not rerender component', () => { - const spy = jest.spyOn(Map.prototype, 'delete'); - let i = 0; - const { result } = renderHook(() => [++i, useMap([['foo', 1]])] as const); - - act(() => { - expect(result.current[1].delete('foo')).toBe(true); - expect(spy).toHaveBeenCalledWith('foo'); - }); - - expect(result.current[0]).toBe(1); - - spy.mockRestore(); - }); + it('should be defined', () => { + expect(useMap).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useMap()); + expect(result.error).toBeUndefined(); + }); + + it('should return a Map instance with altered set, clear and delete methods', () => { + const { result } = renderHook(() => useMap()); + expect(result.current).toBeInstanceOf(Map); + expect(result.current.set).not.toBe(Map.prototype.set); + expect(result.current.clear).not.toBe(Map.prototype.clear); + expect(result.current.delete).not.toBe(Map.prototype.delete); + }); + + it('should accept initial values', () => { + const { result } = renderHook(() => + useMap([ + ['foo', 1], + ['bar', 2], + ['baz', 3], + ]) + ); + expect(result.current.get('foo')).toBe(1); + expect(result.current.get('bar')).toBe(2); + expect(result.current.get('baz')).toBe(3); + expect(result.current.size).toBe(3); + }); + + it('`set` should invoke original method and not rerender component', () => { + const spy = jest.spyOn(Map.prototype, 'set'); + let i = 0; + const { result } = renderHook(() => [++i, useMap()] as const); + + act(() => { + expect(result.current[1].set('foo', 'bar')).toBe(result.current[1]); + expect(spy).toHaveBeenCalledWith('foo', 'bar'); + }); + + expect(result.current[0]).toBe(1); + + spy.mockRestore(); + }); + + it('`clear` should invoke original method and not rerender component', () => { + const spy = jest.spyOn(Map.prototype, 'clear'); + let i = 0; + const { result } = renderHook(() => [++i, useMap()] as const); + + act(() => { + result.current[1].clear(); + }); + + expect(result.current[0]).toBe(1); + + spy.mockRestore(); + }); + + it('`delete` should invoke original method and not rerender component', () => { + const spy = jest.spyOn(Map.prototype, 'delete'); + let i = 0; + const { result } = renderHook(() => [++i, useMap([['foo', 1]])] as const); + + act(() => { + expect(result.current[1].delete('foo')).toBe(true); + expect(spy).toHaveBeenCalledWith('foo'); + }); + + expect(result.current[0]).toBe(1); + + spy.mockRestore(); + }); }); diff --git a/src/useMap/index.ts b/src/useMap/index.ts index 87641efe3..48d7e4b20 100644 --- a/src/useMap/index.ts +++ b/src/useMap/index.ts @@ -10,34 +10,34 @@ const proto = Map.prototype; */ export function useMap( - entries?: ReadonlyArray | null + entries?: ReadonlyArray | null ): Map { - const mapRef = useRef>(); - const rerender = useRerender(); + const mapRef = useRef>(); + const rerender = useRerender(); - if (!mapRef.current) { - const map = new Map(entries); + if (!mapRef.current) { + const map = new Map(entries); - mapRef.current = map; + mapRef.current = map; - map.set = (...args) => { - proto.set.apply(map, args); - rerender(); - return map; - }; + map.set = (...args) => { + proto.set.apply(map, args); + rerender(); + return map; + }; - map.clear = (...args) => { - proto.clear.apply(map, args); - rerender(); - }; + map.clear = (...args) => { + proto.clear.apply(map, args); + rerender(); + }; - map.delete = (...args) => { - const res = proto.delete.apply(map, args); - rerender(); + map.delete = (...args) => { + const res = proto.delete.apply(map, args); + rerender(); - return res; - }; - } + return res; + }; + } - return mapRef.current; + return mapRef.current; } diff --git a/src/useMeasure/__docs__/example.stories.tsx b/src/useMeasure/__docs__/example.stories.tsx index e677fdb19..a1c58cd3a 100644 --- a/src/useMeasure/__docs__/example.stories.tsx +++ b/src/useMeasure/__docs__/example.stories.tsx @@ -2,22 +2,22 @@ import * as React from 'react'; import { useMeasure } from '../..'; export function Example() { - const [measurements, ref] = useMeasure(); + const [measurements, ref] = useMeasure(); - return ( -
-
{JSON.stringify(measurements)}
-
- resize me UwU -
-
- ); + return ( +
+
{JSON.stringify(measurements)}
+
+ resize me UwU +
+
+ ); } diff --git a/src/useMeasure/__docs__/story.mdx b/src/useMeasure/__docs__/story.mdx index 52f9829cc..fdac995c5 100644 --- a/src/useMeasure/__docs__/story.mdx +++ b/src/useMeasure/__docs__/story.mdx @@ -16,15 +16,15 @@ Uses ResizeObserver to track element dimensions and re-render component when the #### Example - + ## Reference ```ts interface Measures { - width: number; - height: number; + width: number; + height: number; } function useMeasure(enabled = true): [Measures | undefined, React.RefObject]; diff --git a/src/useMeasure/__tests__/dom.ts b/src/useMeasure/__tests__/dom.ts index 6a0a7e117..cc710777f 100644 --- a/src/useMeasure/__tests__/dom.ts +++ b/src/useMeasure/__tests__/dom.ts @@ -4,105 +4,105 @@ import { useMeasure } from '../..'; import Mock = jest.Mock; describe('useMeasure', () => { - const raf = global.requestAnimationFrame; - const caf = global.cancelAnimationFrame; - const observeSpy = jest.fn(); - const unobserveSpy = jest.fn(); - const disconnectSpy = jest.fn(); + const raf = global.requestAnimationFrame; + const caf = global.cancelAnimationFrame; + const observeSpy = jest.fn(); + const unobserveSpy = jest.fn(); + const disconnectSpy = jest.fn(); - let ResizeObserverSpy: Mock; - const initialRO = global.ResizeObserver; + let ResizeObserverSpy: Mock; + const initialRO = global.ResizeObserver; - beforeAll(() => { - jest.useFakeTimers(); + beforeAll(() => { + jest.useFakeTimers(); - global.requestAnimationFrame = (cb) => setTimeout(cb, 1); - global.cancelAnimationFrame = (cb) => { - clearTimeout(cb); - }; + global.requestAnimationFrame = (cb) => setTimeout(cb, 1); + global.cancelAnimationFrame = (cb) => { + clearTimeout(cb); + }; - ResizeObserverSpy = jest.fn(() => ({ - observe: observeSpy, - unobserve: unobserveSpy, - disconnect: disconnectSpy, - })); + ResizeObserverSpy = jest.fn(() => ({ + observe: observeSpy, + unobserve: unobserveSpy, + disconnect: disconnectSpy, + })); - global.ResizeObserver = ResizeObserverSpy; - }); + global.ResizeObserver = ResizeObserverSpy; + }); - beforeEach(() => { - observeSpy.mockClear(); - unobserveSpy.mockClear(); - disconnectSpy.mockClear(); - }); + beforeEach(() => { + observeSpy.mockClear(); + unobserveSpy.mockClear(); + disconnectSpy.mockClear(); + }); - afterEach(() => { - jest.clearAllTimers(); - }); + afterEach(() => { + jest.clearAllTimers(); + }); - afterAll(() => { - jest.useRealTimers(); + afterAll(() => { + jest.useRealTimers(); - global.ResizeObserver = initialRO; + global.ResizeObserver = initialRO; - global.requestAnimationFrame = raf; - global.cancelAnimationFrame = caf; - }); + global.requestAnimationFrame = raf; + global.cancelAnimationFrame = caf; + }); - it('should be defined', () => { - expect(useMeasure).toBeDefined(); - }); + it('should be defined', () => { + expect(useMeasure).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useMeasure()); + it('should render', () => { + const { result } = renderHook(() => useMeasure()); - expect(result.error).toBeUndefined(); - }); + expect(result.error).toBeUndefined(); + }); - it('should return undefined sate on initial render', () => { - const { result } = renderHook(() => useMeasure()); + it('should return undefined sate on initial render', () => { + const { result } = renderHook(() => useMeasure()); - expect(result.current[0]).toBeUndefined(); - }); + expect(result.current[0]).toBeUndefined(); + }); - it('should return reference as a second array element', () => { - const { result } = renderHook(() => useMeasure()); + it('should return reference as a second array element', () => { + const { result } = renderHook(() => useMeasure()); - expect(result.current[1]).toStrictEqual({ current: null }); - }); + expect(result.current[1]).toStrictEqual({ current: null }); + }); - it('should only set state within animation frame', () => { - const div = document.createElement('div'); - const { result } = renderHook(() => { - const res = useMeasure(); + it('should only set state within animation frame', () => { + const div = document.createElement('div'); + const { result } = renderHook(() => { + const res = useMeasure(); - useEffect(() => { - res[1].current = div; - }); + useEffect(() => { + res[1].current = div; + }); - return res; - }); + return res; + }); - const measures = { - width: 0, - height: 0, - }; + const measures = { + width: 0, + height: 0, + }; - const entry = { - target: div, - contentRect: { width: 0, height: 0 }, - borderBoxSize: {}, - contentBoxSize: {}, - } as unknown as ResizeObserverEntry; + const entry = { + target: div, + contentRect: { width: 0, height: 0 }, + borderBoxSize: {}, + contentBoxSize: {}, + } as unknown as ResizeObserverEntry; - ResizeObserverSpy.mock.calls[0][0]([entry]); - expect(result.current[0]).toBeUndefined(); + ResizeObserverSpy.mock.calls[0][0]([entry]); + expect(result.current[0]).toBeUndefined(); - act(() => { - jest.advanceTimersByTime(1); - }); + act(() => { + jest.advanceTimersByTime(1); + }); - expect(result.current[1]).toStrictEqual({ current: div }); - expect(result.current[0]).toStrictEqual(measures); - }); + expect(result.current[1]).toStrictEqual({ current: div }); + expect(result.current[0]).toStrictEqual(measures); + }); }); diff --git a/src/useMeasure/__tests__/ssr.ts b/src/useMeasure/__tests__/ssr.ts index 6eb9d2f7a..24cac5270 100644 --- a/src/useMeasure/__tests__/ssr.ts +++ b/src/useMeasure/__tests__/ssr.ts @@ -3,52 +3,52 @@ import { useMeasure } from '../..'; import Mock = jest.Mock; describe('useMeasure', () => { - const observeSpy = jest.fn(); - const unobserveSpy = jest.fn(); - const disconnectSpy = jest.fn(); + const observeSpy = jest.fn(); + const unobserveSpy = jest.fn(); + const disconnectSpy = jest.fn(); - let ResizeObserverSpy: Mock; - const initialRO = global.ResizeObserver; + let ResizeObserverSpy: Mock; + const initialRO = global.ResizeObserver; - beforeAll(() => { - ResizeObserverSpy = jest.fn(() => ({ - observe: observeSpy, - unobserve: unobserveSpy, - disconnect: disconnectSpy, - })); + beforeAll(() => { + ResizeObserverSpy = jest.fn(() => ({ + observe: observeSpy, + unobserve: unobserveSpy, + disconnect: disconnectSpy, + })); - global.ResizeObserver = ResizeObserverSpy; - }); + global.ResizeObserver = ResizeObserverSpy; + }); - beforeEach(() => { - observeSpy.mockClear(); - unobserveSpy.mockClear(); - disconnectSpy.mockClear(); - }); + beforeEach(() => { + observeSpy.mockClear(); + unobserveSpy.mockClear(); + disconnectSpy.mockClear(); + }); - afterAll(() => { - global.ResizeObserver = initialRO; - }); + afterAll(() => { + global.ResizeObserver = initialRO; + }); - it('should be defined', () => { - expect(useMeasure).toBeDefined(); - }); + it('should be defined', () => { + expect(useMeasure).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useMeasure()); + it('should render', () => { + const { result } = renderHook(() => useMeasure()); - expect(result.error).toBeUndefined(); - }); + expect(result.error).toBeUndefined(); + }); - it('should return undefined sate on initial render', () => { - const { result } = renderHook(() => useMeasure()); + it('should return undefined sate on initial render', () => { + const { result } = renderHook(() => useMeasure()); - expect(result.current[0]).toBeUndefined(); - }); + expect(result.current[0]).toBeUndefined(); + }); - it('should return reference as a second array element', () => { - const { result } = renderHook(() => useMeasure()); + it('should return reference as a second array element', () => { + const { result } = renderHook(() => useMeasure()); - expect(result.current[1]).toStrictEqual({ current: null }); - }); + expect(result.current[1]).toStrictEqual({ current: null }); + }); }); diff --git a/src/useMeasure/index.ts b/src/useMeasure/index.ts index a64bef864..834d6d35b 100644 --- a/src/useMeasure/index.ts +++ b/src/useMeasure/index.ts @@ -4,8 +4,8 @@ import { useHookableRef } from '../useHookableRef'; import { useRafCallback } from '../useRafCallback'; export type Measures = { - width: number; - height: number; + width: number; + height: number; }; /** @@ -14,21 +14,21 @@ export type Measures = { * @param enabled Whether resize observer is enabled or not. */ export function useMeasure( - enabled = true + enabled = true ): [Measures | undefined, MutableRefObject] { - const [element, setElement] = useState(null); - const elementRef = useHookableRef(null, (v) => { - setElement(v); + const [element, setElement] = useState(null); + const elementRef = useHookableRef(null, (v) => { + setElement(v); - return v; - }); + return v; + }); - const [measures, setMeasures] = useState(); - const [observerHandler] = useRafCallback((entry) => { - setMeasures({ width: entry.contentRect.width, height: entry.contentRect.height }); - }); + const [measures, setMeasures] = useState(); + const [observerHandler] = useRafCallback((entry) => { + setMeasures({ width: entry.contentRect.width, height: entry.contentRect.height }); + }); - useResizeObserver(element, observerHandler, enabled); + useResizeObserver(element, observerHandler, enabled); - return [measures, elementRef]; + return [measures, elementRef]; } diff --git a/src/useMediaQuery/__docs__/example.stories.tsx b/src/useMediaQuery/__docs__/example.stories.tsx index 930dfa078..22202af65 100644 --- a/src/useMediaQuery/__docs__/example.stories.tsx +++ b/src/useMediaQuery/__docs__/example.stories.tsx @@ -2,36 +2,36 @@ import * as React from 'react'; import { useMediaQuery } from '../..'; export function Example() { - const isSmallDevice = useMediaQuery('only screen and (max-width : 768px)'); - const isMediumDevice = useMediaQuery( - 'only screen and (min-width : 769px) and (max-width : 992px)' - ); - const isLargeDevice = useMediaQuery( - 'only screen and (min-width : 993px) and (max-width : 1200px)' - ); - const isExtraLargeDevice = useMediaQuery('only screen and (min-width : 1201px)'); + const isSmallDevice = useMediaQuery('only screen and (max-width : 768px)'); + const isMediumDevice = useMediaQuery( + 'only screen and (min-width : 769px) and (max-width : 992px)' + ); + const isLargeDevice = useMediaQuery( + 'only screen and (min-width : 993px) and (max-width : 1200px)' + ); + const isExtraLargeDevice = useMediaQuery('only screen and (min-width : 1201px)'); - return ( -
- Resize your browser windows to see changes. -
-
-
- Small device (max-width : 768px):{' '} - {isSmallDevice === undefined ? 'unknown' : isSmallDevice ? 'yes' : 'no'} -
-
- Medium device (max-width : 992px):{' '} - {isMediumDevice === undefined ? 'unknown' : isMediumDevice ? 'yes' : 'no'} -
-
- Large device (max-width : 1200px):{' '} - {isLargeDevice === undefined ? 'unknown' : isLargeDevice ? 'yes' : 'no'} -
-
- Extra large device (min-width : 1201px):{' '} - {isExtraLargeDevice === undefined ? 'unknown' : isExtraLargeDevice ? 'yes' : 'no'} -
-
- ); + return ( +
+ Resize your browser windows to see changes. +
+
+
+ Small device (max-width : 768px):{' '} + {isSmallDevice === undefined ? 'unknown' : isSmallDevice ? 'yes' : 'no'} +
+
+ Medium device (max-width : 992px):{' '} + {isMediumDevice === undefined ? 'unknown' : isMediumDevice ? 'yes' : 'no'} +
+
+ Large device (max-width : 1200px):{' '} + {isLargeDevice === undefined ? 'unknown' : isLargeDevice ? 'yes' : 'no'} +
+
+ Extra large device (min-width : 1201px):{' '} + {isExtraLargeDevice === undefined ? 'unknown' : isExtraLargeDevice ? 'yes' : 'no'} +
+
+ ); } diff --git a/src/useMediaQuery/__docs__/story.mdx b/src/useMediaQuery/__docs__/story.mdx index 08eb1a1f9..61c9d3556 100644 --- a/src/useMediaQuery/__docs__/story.mdx +++ b/src/useMediaQuery/__docs__/story.mdx @@ -16,14 +16,14 @@ Tracks the state of CSS media query. #### Example - + ## Reference ```ts interface UseMediaQueryOptions { - initializeWithValue?: boolean; + initializeWithValue?: boolean; } export function useMediaQuery(query: string, options?: UseMediaQueryOptions): boolean | undefined; diff --git a/src/useMediaQuery/__tests__/dom.ts b/src/useMediaQuery/__tests__/dom.ts index 61fabdea4..8814e73f2 100644 --- a/src/useMediaQuery/__tests__/dom.ts +++ b/src/useMediaQuery/__tests__/dom.ts @@ -2,171 +2,171 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { useMediaQuery } from '../..'; describe('useMediaQuery', () => { - type MutableMediaQueryList = { - matches: boolean; - media: string; - onchange: null; - addListener: jest.Mock; // Deprecated - removeListener: jest.Mock; // Deprecated - addEventListener: jest.Mock; - removeEventListener: jest.Mock; - dispatchEvent: jest.Mock; - }; - - const matchMediaMock = jest.fn(); - let initialMatchMedia: typeof window.matchMedia; - - beforeAll(() => { - initialMatchMedia = window.matchMedia; - Object.defineProperty(window, 'matchMedia', { - writable: true, - value: matchMediaMock, - }); - }); - - afterAll(() => { - window.matchMedia = initialMatchMedia; - }); - - beforeEach(() => { - matchMediaMock.mockImplementation((query) => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), // Deprecated - removeListener: jest.fn(), // Deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })); - }); - - afterEach(() => { - matchMediaMock.mockClear(); - }); - - it('should be defined', () => { - expect(useMediaQuery).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useMediaQuery('max-width : 768px')); - expect(result.error).toBeUndefined(); - }); - - it('should return undefined on first render, if initializeWithValue is false', () => { - const { result } = renderHook(() => - useMediaQuery('max-width : 768px', { initializeWithValue: false }) - ); - expect(result.all.length).toBe(2); - expect(result.all[0]).toBe(undefined); - expect(result.current).toBe(false); - }); - - it('should return value on first render, if initializeWithValue is true', () => { - const { result } = renderHook(() => - useMediaQuery('max-width : 768px', { initializeWithValue: true }) - ); - expect(result.all.length).toBe(1); - expect(result.current).toBe(false); - }); - - it('should return match state', () => { - const { result } = renderHook(() => useMediaQuery('max-width : 768px')); - expect(result.current).toBe(false); - }); - - it('should update state if query state changed', () => { - const { result } = renderHook(() => useMediaQuery('max-width : 768px')); - expect(result.current).toBe(false); - - const mql = matchMediaMock.mock.results[0].value as MutableMediaQueryList; - mql.matches = true; - - act(() => { - mql.addEventListener.mock.calls[0][1](); - }); - expect(result.current).toBe(true); - }); - - it('several hooks tracking same rule must listen same mql', () => { - const { result: result1 } = renderHook(() => useMediaQuery('max-width : 768px')); - const { result: result2 } = renderHook(() => useMediaQuery('max-width : 768px')); - const { result: result3 } = renderHook(() => useMediaQuery('max-width : 768px')); - expect(result1.current).toBe(false); - expect(result2.current).toBe(false); - expect(result3.current).toBe(false); - - const mql = matchMediaMock.mock.results[0].value as MutableMediaQueryList; - mql.matches = true; - - act(() => { - mql.addEventListener.mock.calls[0][1](); - }); - expect(result1.current).toBe(true); - expect(result2.current).toBe(true); - expect(result3.current).toBe(true); - }); - - it('should unsubscribe from previous mql when query changed', () => { - const { result: result1 } = renderHook(() => useMediaQuery('max-width : 768px')); - const { result: result2 } = renderHook(() => useMediaQuery('max-width : 768px')); - const { result: result3, rerender: rerender3 } = renderHook( - ({ query }) => useMediaQuery(query), - { - initialProps: { query: 'max-width : 768px' }, - } - ); - expect(result1.current).toBe(false); - expect(result2.current).toBe(false); - expect(result3.current).toBe(false); - - rerender3({ query: 'max-width : 760px' }); - - expect(matchMediaMock).toHaveBeenCalledTimes(2); - - const mql = matchMediaMock.mock.results[0].value as MutableMediaQueryList; - mql.matches = true; - - act(() => { - mql.addEventListener.mock.calls[0][1](); - }); - expect(result1.current).toBe(true); - expect(result2.current).toBe(true); - expect(result3.current).toBe(false); - }); - - it('should unsubscribe from mql only when no hooks are awaiting such value', () => { - const { unmount: unmount1 } = renderHook(() => useMediaQuery('max-width : 768px')); - const { unmount: unmount2 } = renderHook(() => useMediaQuery('max-width : 768px')); - const { unmount: unmount3 } = renderHook(() => useMediaQuery('max-width : 768px')); - - const mql = matchMediaMock.mock.results[0].value as MutableMediaQueryList; - expect(mql.removeEventListener).not.toHaveBeenCalled(); - unmount3(); - expect(mql.removeEventListener).not.toHaveBeenCalled(); - unmount2(); - expect(mql.removeEventListener).not.toHaveBeenCalled(); - unmount1(); - expect(mql.removeEventListener).toHaveBeenCalledTimes(1); - }); - - it('should use addListener and removeListener in case of absence of modern methods', () => { - matchMediaMock.mockImplementation((query) => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), - removeListener: jest.fn(), - dispatchEvent: jest.fn(), - })); - - const { unmount } = renderHook(() => useMediaQuery('max-width : 1024px')); - - const mql = matchMediaMock.mock.results[0].value as MutableMediaQueryList; - expect(mql.addListener).toHaveBeenCalledTimes(1); - - unmount(); - expect(mql.removeListener).toHaveBeenCalledTimes(1); - }); + type MutableMediaQueryList = { + matches: boolean; + media: string; + onchange: null; + addListener: jest.Mock; // Deprecated + removeListener: jest.Mock; // Deprecated + addEventListener: jest.Mock; + removeEventListener: jest.Mock; + dispatchEvent: jest.Mock; + }; + + const matchMediaMock = jest.fn(); + let initialMatchMedia: typeof window.matchMedia; + + beforeAll(() => { + initialMatchMedia = window.matchMedia; + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: matchMediaMock, + }); + }); + + afterAll(() => { + window.matchMedia = initialMatchMedia; + }); + + beforeEach(() => { + matchMediaMock.mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })); + }); + + afterEach(() => { + matchMediaMock.mockClear(); + }); + + it('should be defined', () => { + expect(useMediaQuery).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useMediaQuery('max-width : 768px')); + expect(result.error).toBeUndefined(); + }); + + it('should return undefined on first render, if initializeWithValue is false', () => { + const { result } = renderHook(() => + useMediaQuery('max-width : 768px', { initializeWithValue: false }) + ); + expect(result.all.length).toBe(2); + expect(result.all[0]).toBe(undefined); + expect(result.current).toBe(false); + }); + + it('should return value on first render, if initializeWithValue is true', () => { + const { result } = renderHook(() => + useMediaQuery('max-width : 768px', { initializeWithValue: true }) + ); + expect(result.all.length).toBe(1); + expect(result.current).toBe(false); + }); + + it('should return match state', () => { + const { result } = renderHook(() => useMediaQuery('max-width : 768px')); + expect(result.current).toBe(false); + }); + + it('should update state if query state changed', () => { + const { result } = renderHook(() => useMediaQuery('max-width : 768px')); + expect(result.current).toBe(false); + + const mql = matchMediaMock.mock.results[0].value as MutableMediaQueryList; + mql.matches = true; + + act(() => { + mql.addEventListener.mock.calls[0][1](); + }); + expect(result.current).toBe(true); + }); + + it('several hooks tracking same rule must listen same mql', () => { + const { result: result1 } = renderHook(() => useMediaQuery('max-width : 768px')); + const { result: result2 } = renderHook(() => useMediaQuery('max-width : 768px')); + const { result: result3 } = renderHook(() => useMediaQuery('max-width : 768px')); + expect(result1.current).toBe(false); + expect(result2.current).toBe(false); + expect(result3.current).toBe(false); + + const mql = matchMediaMock.mock.results[0].value as MutableMediaQueryList; + mql.matches = true; + + act(() => { + mql.addEventListener.mock.calls[0][1](); + }); + expect(result1.current).toBe(true); + expect(result2.current).toBe(true); + expect(result3.current).toBe(true); + }); + + it('should unsubscribe from previous mql when query changed', () => { + const { result: result1 } = renderHook(() => useMediaQuery('max-width : 768px')); + const { result: result2 } = renderHook(() => useMediaQuery('max-width : 768px')); + const { result: result3, rerender: rerender3 } = renderHook( + ({ query }) => useMediaQuery(query), + { + initialProps: { query: 'max-width : 768px' }, + } + ); + expect(result1.current).toBe(false); + expect(result2.current).toBe(false); + expect(result3.current).toBe(false); + + rerender3({ query: 'max-width : 760px' }); + + expect(matchMediaMock).toHaveBeenCalledTimes(2); + + const mql = matchMediaMock.mock.results[0].value as MutableMediaQueryList; + mql.matches = true; + + act(() => { + mql.addEventListener.mock.calls[0][1](); + }); + expect(result1.current).toBe(true); + expect(result2.current).toBe(true); + expect(result3.current).toBe(false); + }); + + it('should unsubscribe from mql only when no hooks are awaiting such value', () => { + const { unmount: unmount1 } = renderHook(() => useMediaQuery('max-width : 768px')); + const { unmount: unmount2 } = renderHook(() => useMediaQuery('max-width : 768px')); + const { unmount: unmount3 } = renderHook(() => useMediaQuery('max-width : 768px')); + + const mql = matchMediaMock.mock.results[0].value as MutableMediaQueryList; + expect(mql.removeEventListener).not.toHaveBeenCalled(); + unmount3(); + expect(mql.removeEventListener).not.toHaveBeenCalled(); + unmount2(); + expect(mql.removeEventListener).not.toHaveBeenCalled(); + unmount1(); + expect(mql.removeEventListener).toHaveBeenCalledTimes(1); + }); + + it('should use addListener and removeListener in case of absence of modern methods', () => { + matchMediaMock.mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + dispatchEvent: jest.fn(), + })); + + const { unmount } = renderHook(() => useMediaQuery('max-width : 1024px')); + + const mql = matchMediaMock.mock.results[0].value as MutableMediaQueryList; + expect(mql.addListener).toHaveBeenCalledTimes(1); + + unmount(); + expect(mql.removeListener).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/useMediaQuery/__tests__/ssr.ts b/src/useMediaQuery/__tests__/ssr.ts index 3f07e03bd..f572fbdaf 100644 --- a/src/useMediaQuery/__tests__/ssr.ts +++ b/src/useMediaQuery/__tests__/ssr.ts @@ -2,21 +2,21 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useMediaQuery } from '../..'; describe('useMediaQuery', () => { - it('should be defined', () => { - expect(useMediaQuery).toBeDefined(); - }); + it('should be defined', () => { + expect(useMediaQuery).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => - useMediaQuery('max-width : 768px', { initializeWithValue: false }) - ); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => + useMediaQuery('max-width : 768px', { initializeWithValue: false }) + ); + expect(result.error).toBeUndefined(); + }); - it('should return undefined on first render, if initializeWithValue is set to false', () => { - const { result } = renderHook(() => - useMediaQuery('max-width : 768px', { initializeWithValue: false }) - ); - expect(result.current).toBeUndefined(); - }); + it('should return undefined on first render, if initializeWithValue is set to false', () => { + const { result } = renderHook(() => + useMediaQuery('max-width : 768px', { initializeWithValue: false }) + ); + expect(result.current).toBeUndefined(); + }); }); diff --git a/src/useMediaQuery/index.ts b/src/useMediaQuery/index.ts index 8ef146d5d..9854e52a9 100644 --- a/src/useMediaQuery/index.ts +++ b/src/useMediaQuery/index.ts @@ -2,63 +2,63 @@ import { type Dispatch, useEffect, useState } from 'react'; import { isBrowser } from '../util/const'; const queriesMap = new Map< - string, - { mql: MediaQueryList; dispatchers: Set>; listener: () => void } + string, + { mql: MediaQueryList; dispatchers: Set>; listener: () => void } >(); type QueryStateSetter = (matches: boolean) => void; const createQueryEntry = (query: string) => { - const mql = matchMedia(query); - const dispatchers = new Set(); - const listener = () => { - dispatchers.forEach((d) => { - d(mql.matches); - }); - }; - - if (mql.addEventListener) mql.addEventListener('change', listener, { passive: true }); - else mql.addListener(listener); - - return { - mql, - dispatchers, - listener, - }; + const mql = matchMedia(query); + const dispatchers = new Set(); + const listener = () => { + dispatchers.forEach((d) => { + d(mql.matches); + }); + }; + + if (mql.addEventListener) mql.addEventListener('change', listener, { passive: true }); + else mql.addListener(listener); + + return { + mql, + dispatchers, + listener, + }; }; const querySubscribe = (query: string, setState: QueryStateSetter) => { - let entry = queriesMap.get(query); + let entry = queriesMap.get(query); - if (!entry) { - entry = createQueryEntry(query); - queriesMap.set(query, entry); - } + if (!entry) { + entry = createQueryEntry(query); + queriesMap.set(query, entry); + } - entry.dispatchers.add(setState); - setState(entry.mql.matches); + entry.dispatchers.add(setState); + setState(entry.mql.matches); }; const queryUnsubscribe = (query: string, setState: QueryStateSetter): void => { - const entry = queriesMap.get(query); + const entry = queriesMap.get(query); - // Else path is impossible to test in normal situation - /* istanbul ignore else */ - if (entry) { - const { mql, dispatchers, listener } = entry; - dispatchers.delete(setState); + // Else path is impossible to test in normal situation + /* istanbul ignore else */ + if (entry) { + const { mql, dispatchers, listener } = entry; + dispatchers.delete(setState); - if (!dispatchers.size) { - queriesMap.delete(query); + if (!dispatchers.size) { + queriesMap.delete(query); - if (mql.removeEventListener) mql.removeEventListener('change', listener); - else mql.removeListener(listener); - } - } + if (mql.removeEventListener) mql.removeEventListener('change', listener); + else mql.removeListener(listener); + } + } }; type UseMediaQueryOptions = { - initializeWithValue?: boolean; + initializeWithValue?: boolean; }; /** @@ -70,34 +70,34 @@ type UseMediaQueryOptions = { * this to false will make the hook yield `undefined` on first render. */ export function useMediaQuery( - query: string, - options: UseMediaQueryOptions = {} + query: string, + options: UseMediaQueryOptions = {} ): boolean | undefined { - let { initializeWithValue = true } = options; + let { initializeWithValue = true } = options; - if (!isBrowser) { - initializeWithValue = false; - } + if (!isBrowser) { + initializeWithValue = false; + } - const [state, setState] = useState(() => { - if (initializeWithValue) { - let entry = queriesMap.get(query); - if (!entry) { - entry = createQueryEntry(query); - queriesMap.set(query, entry); - } + const [state, setState] = useState(() => { + if (initializeWithValue) { + let entry = queriesMap.get(query); + if (!entry) { + entry = createQueryEntry(query); + queriesMap.set(query, entry); + } - return entry.mql.matches; - } - }); + return entry.mql.matches; + } + }); - useEffect(() => { - querySubscribe(query, setState); + useEffect(() => { + querySubscribe(query, setState); - return () => { - queryUnsubscribe(query, setState); - }; - }, [query]); + return () => { + queryUnsubscribe(query, setState); + }; + }, [query]); - return state; + return state; } diff --git a/src/useMediatedState/__docs__/example.stories.tsx b/src/useMediatedState/__docs__/example.stories.tsx index 198666285..ec62c75cd 100644 --- a/src/useMediatedState/__docs__/example.stories.tsx +++ b/src/useMediatedState/__docs__/example.stories.tsx @@ -2,21 +2,21 @@ import React from 'react'; import { useMediatedState } from '../..'; export function Example() { - const nonLetterRe = /[^a-z]+/gi; - const [state, setState] = useMediatedState('123', (val: string) => - val.replaceAll(nonLetterRe, '') - ); + const nonLetterRe = /[^a-z]+/gi; + const [state, setState] = useMediatedState('123', (val: string) => + val.replaceAll(nonLetterRe, '') + ); - return ( -
-
Below input will only receive letters
- { - setState(ev.currentTarget.value); - }} - /> -
- ); + return ( +
+
Below input will only receive letters
+ { + setState(ev.currentTarget.value); + }} + /> +
+ ); } diff --git a/src/useMediatedState/__docs__/story.mdx b/src/useMediatedState/__docs__/story.mdx index 6ef1ca653..6e0357c39 100644 --- a/src/useMediatedState/__docs__/story.mdx +++ b/src/useMediatedState/__docs__/story.mdx @@ -13,15 +13,15 @@ Like `useState`, but every value (including initial) set is passed through media #### Example - + ## Reference ```ts function useMediatedState( - initialState?: S | (() => S), - mediator?: (state: R) => S + initialState?: S | (() => S), + mediator?: (state: R) => S ): [S, (value: R | ((prevState: S) => R)) => void]; ``` diff --git a/src/useMediatedState/__tests__/dom.ts b/src/useMediatedState/__tests__/dom.ts index b0564f5c0..afaf22da4 100644 --- a/src/useMediatedState/__tests__/dom.ts +++ b/src/useMediatedState/__tests__/dom.ts @@ -2,58 +2,58 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { useMediatedState } from '../..'; describe('useMediatedState', () => { - it('should be defined', () => { - expect(useMediatedState).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useMediatedState()); - expect(result.error).toBeUndefined(); - }); - - it('should act like useState if mediator not passed', () => { - const { result } = renderHook(() => useMediatedState(123)); - - expect(result.current[0]).toBe(123); - act(() => { - result.current[1](321); - }); - expect(result.current[0]).toBe(321); - }); - - it('should pass received sate through mediator', () => { - const spy = jest.fn((val: string) => Number.parseInt(val, 10)); - const { result } = renderHook(() => useMediatedState(123, spy)); - - expect(result.current[0]).toBe(123); - act(() => { - result.current[1]('321'); - }); - expect(result.current[0]).toBe(321); - expect(spy).toHaveBeenCalledTimes(2); - expect(spy).toHaveBeenCalledWith('321'); - }); - - it('should pass initial sate through mediator', () => { - const { result } = renderHook(() => - useMediatedState('a123', (val: string) => val.replaceAll(/[^a-z]+/gi, '')) - ); - - expect(result.current[0]).toBe('a'); - }); - - it('should return same setState method each render even if callback is changed', () => { - const { result, rerender } = renderHook(() => - useMediatedState(123, (val: string) => Number.parseInt(val, 10)) - ); - - const f1 = result.current[1]; - rerender(); - const f2 = result.current[1]; - rerender(); - const f3 = result.current[1]; - - expect(f1).toBe(f2); - expect(f3).toBe(f2); - }); + it('should be defined', () => { + expect(useMediatedState).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useMediatedState()); + expect(result.error).toBeUndefined(); + }); + + it('should act like useState if mediator not passed', () => { + const { result } = renderHook(() => useMediatedState(123)); + + expect(result.current[0]).toBe(123); + act(() => { + result.current[1](321); + }); + expect(result.current[0]).toBe(321); + }); + + it('should pass received sate through mediator', () => { + const spy = jest.fn((val: string) => Number.parseInt(val, 10)); + const { result } = renderHook(() => useMediatedState(123, spy)); + + expect(result.current[0]).toBe(123); + act(() => { + result.current[1]('321'); + }); + expect(result.current[0]).toBe(321); + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledWith('321'); + }); + + it('should pass initial sate through mediator', () => { + const { result } = renderHook(() => + useMediatedState('a123', (val: string) => val.replaceAll(/[^a-z]+/gi, '')) + ); + + expect(result.current[0]).toBe('a'); + }); + + it('should return same setState method each render even if callback is changed', () => { + const { result, rerender } = renderHook(() => + useMediatedState(123, (val: string) => Number.parseInt(val, 10)) + ); + + const f1 = result.current[1]; + rerender(); + const f2 = result.current[1]; + rerender(); + const f3 = result.current[1]; + + expect(f1).toBe(f2); + expect(f3).toBe(f2); + }); }); diff --git a/src/useMediatedState/__tests__/ssr.ts b/src/useMediatedState/__tests__/ssr.ts index 717ea4383..c026ee261 100644 --- a/src/useMediatedState/__tests__/ssr.ts +++ b/src/useMediatedState/__tests__/ssr.ts @@ -2,24 +2,24 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useMediatedState } from '../..'; describe('useMediatedState', () => { - it('should be defined', () => { - expect(useMediatedState).toBeDefined(); - }); + it('should be defined', () => { + expect(useMediatedState).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useMediatedState()); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useMediatedState()); + expect(result.error).toBeUndefined(); + }); - it('should return initial state on first mount', () => { - const { result } = renderHook(() => useMediatedState(123)); + it('should return initial state on first mount', () => { + const { result } = renderHook(() => useMediatedState(123)); - expect(result.current[0]).toBe(123); + expect(result.current[0]).toBe(123); - const { result: result2 } = renderHook(() => - useMediatedState(123, (val: string) => Number.parseInt(val, 10)) - ); + const { result: result2 } = renderHook(() => + useMediatedState(123, (val: string) => Number.parseInt(val, 10)) + ); - expect(result2.current[0]).toBe(123); - }); + expect(result2.current[0]).toBe(123); + }); }); diff --git a/src/useMediatedState/index.ts b/src/useMediatedState/index.ts index 9ad290b13..8ed0532f3 100644 --- a/src/useMediatedState/index.ts +++ b/src/useMediatedState/index.ts @@ -3,41 +3,41 @@ import { useSyncedRef } from '../useSyncedRef'; import { type InitialState, type NextState, resolveHookState } from '../util/resolveHookState'; export function useMediatedState(): [ - State | undefined, - Dispatch> + State | undefined, + Dispatch> ]; export function useMediatedState( - initialState: InitialState + initialState: InitialState ): [State, Dispatch>]; export function useMediatedState( - initialState: InitialState, - mediator?: (state: RawState) => State + initialState: InitialState, + mediator?: (state: RawState) => State ): [State, Dispatch>]; /** * Like `useState`, but every value set is passed through a mediator function. */ export function useMediatedState( - initialState?: InitialState, - mediator?: (state: RawState | State | undefined) => State + initialState?: InitialState, + mediator?: (state: RawState | State | undefined) => State ): [State | undefined, Dispatch>] { - const [state, setState] = useState(() => { - return mediator ? mediator(resolveHookState(initialState)) : initialState; - }); - const mediatorRef = useSyncedRef(mediator); + const [state, setState] = useState(() => { + return mediator ? mediator(resolveHookState(initialState)) : initialState; + }); + const mediatorRef = useSyncedRef(mediator); - return [ - state as State, - useCallback((value) => { - if (mediatorRef.current) { - setState((prevState) => - mediatorRef.current?.( - resolveHookState(value, prevState as State) - ) - ); - } else { - setState(value as unknown as State); - } - }, []), - ]; + return [ + state as State, + useCallback((value) => { + if (mediatorRef.current) { + setState((prevState) => + mediatorRef.current?.( + resolveHookState(value, prevState as State) + ) + ); + } else { + setState(value as unknown as State); + } + }, []), + ]; } diff --git a/src/useMountEffect/__docs__/example.stories.tsx b/src/useMountEffect/__docs__/example.stories.tsx index 78fdd7131..42c96c676 100644 --- a/src/useMountEffect/__docs__/example.stories.tsx +++ b/src/useMountEffect/__docs__/example.stories.tsx @@ -3,23 +3,23 @@ import { useState } from 'react'; import { useRerender, useMountEffect } from '../..'; export function Example() { - const [count, setCount] = useState(0); - const rerender = useRerender(); + const [count, setCount] = useState(0); + const rerender = useRerender(); - useMountEffect(() => { - setCount((i) => i + 1); - }); + useMountEffect(() => { + setCount((i) => i + 1); + }); - return ( -
-
useMountEffect has run {count} time(s)
- -
- ); + return ( +
+
useMountEffect has run {count} time(s)
+ +
+ ); } diff --git a/src/useMountEffect/__docs__/story.mdx b/src/useMountEffect/__docs__/story.mdx index aa444f9da..8c743c6d3 100644 --- a/src/useMountEffect/__docs__/story.mdx +++ b/src/useMountEffect/__docs__/story.mdx @@ -14,7 +14,7 @@ Run effect only when component is first mounted. #### Example - + ## Reference diff --git a/src/useMountEffect/__tests__/dom.ts b/src/useMountEffect/__tests__/dom.ts index f72d378b6..8f63d41e5 100644 --- a/src/useMountEffect/__tests__/dom.ts +++ b/src/useMountEffect/__tests__/dom.ts @@ -2,25 +2,25 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useMountEffect } from '../..'; describe('useMountEffect', () => { - it('should call effector only on first render', () => { - const spy = jest.fn(); + it('should call effector only on first render', () => { + const spy = jest.fn(); - const { result, rerender, unmount } = renderHook(() => { - useMountEffect(spy); - }); + const { result, rerender, unmount } = renderHook(() => { + useMountEffect(spy); + }); - expect(result.current).toBe(undefined); - expect(spy).toHaveBeenCalledTimes(1); + expect(result.current).toBe(undefined); + expect(spy).toHaveBeenCalledTimes(1); - rerender(); - rerender(); - rerender(); - rerender(); + rerender(); + rerender(); + rerender(); + rerender(); - expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledTimes(1); - unmount(); + unmount(); - expect(spy).toHaveBeenCalledTimes(1); - }); + expect(spy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/useMountEffect/__tests__/ssr.ts b/src/useMountEffect/__tests__/ssr.ts index 5b491e698..de22885ba 100644 --- a/src/useMountEffect/__tests__/ssr.ts +++ b/src/useMountEffect/__tests__/ssr.ts @@ -2,13 +2,13 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useMountEffect } from '../..'; describe('useMountEffect', () => { - it('should call effector only on first render', () => { - const spy = jest.fn(); + it('should call effector only on first render', () => { + const spy = jest.fn(); - renderHook(() => { - useMountEffect(spy); - }); + renderHook(() => { + useMountEffect(spy); + }); - expect(spy).toHaveBeenCalledTimes(0); - }); + expect(spy).toHaveBeenCalledTimes(0); + }); }); diff --git a/src/useMountEffect/index.ts b/src/useMountEffect/index.ts index f827cbdc7..4b09bac4c 100644 --- a/src/useMountEffect/index.ts +++ b/src/useMountEffect/index.ts @@ -6,7 +6,7 @@ import { useEffect } from 'react'; * @param effect Effector to run on mount */ export function useMountEffect(effect: CallableFunction): void { - useEffect(() => { - effect(); - }, []); + useEffect(() => { + effect(); + }, []); } diff --git a/src/useNetworkState/__docs__/example.stories.tsx b/src/useNetworkState/__docs__/example.stories.tsx index 3c4ee2607..53ffed387 100644 --- a/src/useNetworkState/__docs__/example.stories.tsx +++ b/src/useNetworkState/__docs__/example.stories.tsx @@ -2,12 +2,12 @@ import React from 'react'; import { useNetworkState } from '../..'; export function Example() { - const onlineState = useNetworkState(); + const onlineState = useNetworkState(); - return ( -
-
Your current internet connection state:
-
{JSON.stringify(onlineState, null, 2)}
-
- ); + return ( +
+
Your current internet connection state:
+
{JSON.stringify(onlineState, null, 2)}
+
+ ); } diff --git a/src/useNetworkState/__docs__/story.mdx b/src/useNetworkState/__docs__/story.mdx index 55022fbb9..3131c01b4 100644 --- a/src/useNetworkState/__docs__/story.mdx +++ b/src/useNetworkState/__docs__/story.mdx @@ -15,31 +15,31 @@ Tracks the state of browser's network connection. #### Example - + ## Reference ```ts export interface IUseNetworkState { - online: boolean | undefined; - previous: boolean | undefined; - since: Date | undefined; - downlink: number | undefined; - downlinkMax: number | undefined; - effectiveType: 'slow-2g' | '2g' | '3g' | '4g' | undefined; - rtt: number | undefined; - saveData: boolean | undefined; - type: - | 'bluetooth' - | 'cellular' - | 'ethernet' - | 'none' - | 'wifi' - | 'wimax' - | 'other' - | 'unknown' - | undefined; + online: boolean | undefined; + previous: boolean | undefined; + since: Date | undefined; + downlink: number | undefined; + downlinkMax: number | undefined; + effectiveType: 'slow-2g' | '2g' | '3g' | '4g' | undefined; + rtt: number | undefined; + saveData: boolean | undefined; + type: + | 'bluetooth' + | 'cellular' + | 'ethernet' + | 'none' + | 'wifi' + | 'wimax' + | 'other' + | 'unknown' + | undefined; } export function useNetworkState(initialState?: InitialState): IUseNetworkState; diff --git a/src/useNetworkState/__tests__/dom.ts b/src/useNetworkState/__tests__/dom.ts index 15e90cc46..2dfaae1a6 100644 --- a/src/useNetworkState/__tests__/dom.ts +++ b/src/useNetworkState/__tests__/dom.ts @@ -3,48 +3,48 @@ import { useRef } from 'react'; import { useNetworkState } from '../..'; describe(`useNetworkState`, () => { - it('should be defined', () => { - expect(useNetworkState).toBeDefined(); - }); + it('should be defined', () => { + expect(useNetworkState).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useNetworkState()); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useNetworkState()); + expect(result.error).toBeUndefined(); + }); - it('should return an object of certain structure', () => { - const hook = renderHook(() => useNetworkState(), { initialProps: false }); + it('should return an object of certain structure', () => { + const hook = renderHook(() => useNetworkState(), { initialProps: false }); - expect(typeof hook.result.current).toEqual('object'); - expect(Object.keys(hook.result.current)).toEqual([ - 'online', - 'previous', - 'since', - 'downlink', - 'downlinkMax', - 'effectiveType', - 'rtt', - 'saveData', - 'type', - ]); - }); + expect(typeof hook.result.current).toEqual('object'); + expect(Object.keys(hook.result.current)).toEqual([ + 'online', + 'previous', + 'since', + 'downlink', + 'downlinkMax', + 'effectiveType', + 'rtt', + 'saveData', + 'type', + ]); + }); - it('should rerender in case of online or offline events emitted on window', () => { - const hook = renderHook( - () => { - const renderCount = useRef(0); - return [useNetworkState(), ++renderCount.current]; - }, - { initialProps: false } - ); + it('should rerender in case of online or offline events emitted on window', () => { + const hook = renderHook( + () => { + const renderCount = useRef(0); + return [useNetworkState(), ++renderCount.current]; + }, + { initialProps: false } + ); - expect(hook.result.current[1]).toBe(1); - const prevNWState = hook.result.current[0]; + expect(hook.result.current[1]).toBe(1); + const prevNWState = hook.result.current[0]; - act(() => { - window.dispatchEvent(new Event('online')); - }); - expect(hook.result.current[1]).toBe(2); - expect(hook.result.current[0]).not.toBe(prevNWState); - }); + act(() => { + window.dispatchEvent(new Event('online')); + }); + expect(hook.result.current[1]).toBe(2); + expect(hook.result.current[0]).not.toBe(prevNWState); + }); }); diff --git a/src/useNetworkState/__tests__/ssr.ts b/src/useNetworkState/__tests__/ssr.ts index 2ab48e030..a1483d594 100644 --- a/src/useNetworkState/__tests__/ssr.ts +++ b/src/useNetworkState/__tests__/ssr.ts @@ -2,27 +2,27 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useNetworkState } from '../..'; describe(`useNetworkState`, () => { - it('should be defined', () => { - expect(useNetworkState).toBeDefined(); - }); - it('should render', () => { - const { result } = renderHook(() => useNetworkState()); - expect(result.error).toBeUndefined(); - }); + it('should be defined', () => { + expect(useNetworkState).toBeDefined(); + }); + it('should render', () => { + const { result } = renderHook(() => useNetworkState()); + expect(result.error).toBeUndefined(); + }); - it('should have undefined state', () => { - const hook = renderHook(() => useNetworkState()); + it('should have undefined state', () => { + const hook = renderHook(() => useNetworkState()); - expect(hook.result.current).toStrictEqual({ - downlink: undefined, - downlinkMax: undefined, - effectiveType: undefined, - online: undefined, - previous: undefined, - rtt: undefined, - saveData: undefined, - since: undefined, - type: undefined, - }); - }); + expect(hook.result.current).toStrictEqual({ + downlink: undefined, + downlinkMax: undefined, + effectiveType: undefined, + online: undefined, + previous: undefined, + rtt: undefined, + saveData: undefined, + since: undefined, + type: undefined, + }); + }); }); diff --git a/src/useNetworkState/index.ts b/src/useNetworkState/index.ts index 84bbb88d3..daadef9a5 100644 --- a/src/useNetworkState/index.ts +++ b/src/useNetworkState/index.ts @@ -4,130 +4,130 @@ import { off, on } from '../util/misc'; import { type InitialState } from '../util/resolveHookState'; export type NetworkInformation = { - readonly downlink: number; - readonly downlinkMax: number; - readonly effectiveType: 'slow-2g' | '2g' | '3g' | '4g'; - readonly rtt: number; - readonly saveData: boolean; - readonly type: - | 'bluetooth' - | 'cellular' - | 'ethernet' - | 'none' - | 'wifi' - | 'wimax' - | 'other' - | 'unknown'; + readonly downlink: number; + readonly downlinkMax: number; + readonly effectiveType: 'slow-2g' | '2g' | '3g' | '4g'; + readonly rtt: number; + readonly saveData: boolean; + readonly type: + | 'bluetooth' + | 'cellular' + | 'ethernet' + | 'none' + | 'wifi' + | 'wimax' + | 'other' + | 'unknown'; } & EventTarget; export type UseNetworkState = { - /** - * @desc Whether browser connected to the network or not. - */ - online: boolean | undefined; - /** - * @desc Previous value of `online` property. Helps to identify if browser - * just connected or lost connection. - */ - previous: boolean | undefined; - /** - * @desc The {Date} object pointing to the moment when state change occurred. - */ - since: Date | undefined; - /** - * @desc Effective bandwidth estimate in megabits per second, rounded to the - * nearest multiple of 25 kilobits per seconds. - */ - downlink: NetworkInformation['downlink'] | undefined; - /** - * @desc Maximum downlink speed, in megabits per second (Mbps), for the - * underlying connection technology - */ - downlinkMax: NetworkInformation['downlinkMax'] | undefined; - /** - * @desc Effective type of the connection meaning one of 'slow-2g', '2g', '3g', or '4g'. - * This value is determined using a combination of recently observed round-trip time - * and downlink values. - */ - effectiveType: NetworkInformation['effectiveType'] | undefined; - /** - * @desc Estimated effective round-trip time of the current connection, rounded - * to the nearest multiple of 25 milliseconds - */ - rtt: NetworkInformation['rtt'] | undefined; - /** - * @desc {true} if the user has set a reduced data usage option on the user agent. - */ - saveData: NetworkInformation['saveData'] | undefined; - /** - * @desc The type of connection a device is using to communicate with the network. - * It will be one of the following values: - * - bluetooth - * - cellular - * - ethernet - * - none - * - wifi - * - wimax - * - other - * - unknown - */ - type: NetworkInformation['type'] | undefined; + /** + * @desc Whether browser connected to the network or not. + */ + online: boolean | undefined; + /** + * @desc Previous value of `online` property. Helps to identify if browser + * just connected or lost connection. + */ + previous: boolean | undefined; + /** + * @desc The {Date} object pointing to the moment when state change occurred. + */ + since: Date | undefined; + /** + * @desc Effective bandwidth estimate in megabits per second, rounded to the + * nearest multiple of 25 kilobits per seconds. + */ + downlink: NetworkInformation['downlink'] | undefined; + /** + * @desc Maximum downlink speed, in megabits per second (Mbps), for the + * underlying connection technology + */ + downlinkMax: NetworkInformation['downlinkMax'] | undefined; + /** + * @desc Effective type of the connection meaning one of 'slow-2g', '2g', '3g', or '4g'. + * This value is determined using a combination of recently observed round-trip time + * and downlink values. + */ + effectiveType: NetworkInformation['effectiveType'] | undefined; + /** + * @desc Estimated effective round-trip time of the current connection, rounded + * to the nearest multiple of 25 milliseconds + */ + rtt: NetworkInformation['rtt'] | undefined; + /** + * @desc {true} if the user has set a reduced data usage option on the user agent. + */ + saveData: NetworkInformation['saveData'] | undefined; + /** + * @desc The type of connection a device is using to communicate with the network. + * It will be one of the following values: + * - bluetooth + * - cellular + * - ethernet + * - none + * - wifi + * - wimax + * - other + * - unknown + */ + type: NetworkInformation['type'] | undefined; }; type NavigatorWithConnection = Navigator & - Partial>; + Partial>; const navigator = isBrowser ? (window.navigator as NavigatorWithConnection) : undefined; const conn: NetworkInformation | undefined = - navigator && (navigator.connection ?? navigator.mozConnection ?? navigator.webkitConnection); + navigator && (navigator.connection ?? navigator.mozConnection ?? navigator.webkitConnection); function getConnectionState(previousState?: UseNetworkState): UseNetworkState { - const online = navigator?.onLine; - const previousOnline = previousState?.online; + const online = navigator?.onLine; + const previousOnline = previousState?.online; - return { - online, - previous: previousOnline, - since: online === previousOnline ? previousState?.since : new Date(), - downlink: conn?.downlink, - downlinkMax: conn?.downlinkMax, - effectiveType: conn?.effectiveType, - rtt: conn?.rtt, - saveData: conn?.saveData, - type: conn?.type, - }; + return { + online, + previous: previousOnline, + since: online === previousOnline ? previousState?.since : new Date(), + downlink: conn?.downlink, + downlinkMax: conn?.downlinkMax, + effectiveType: conn?.effectiveType, + rtt: conn?.rtt, + saveData: conn?.saveData, + type: conn?.type, + }; } /** * Tracks the state of browser's network connection. */ export function useNetworkState(initialState?: InitialState): UseNetworkState { - const [state, setState] = useState(initialState ?? getConnectionState); + const [state, setState] = useState(initialState ?? getConnectionState); - useEffect(() => { - const handleStateChange = () => { - setState(getConnectionState); - }; + useEffect(() => { + const handleStateChange = () => { + setState(getConnectionState); + }; - on(window, 'online', handleStateChange, { passive: true }); - on(window, 'offline', handleStateChange, { passive: true }); + on(window, 'online', handleStateChange, { passive: true }); + on(window, 'offline', handleStateChange, { passive: true }); - // It is quite hard to test it in jsdom environment maybe will be improved in future - /* istanbul ignore next */ - if (conn) { - on(conn, 'change', handleStateChange, { passive: true }); - } + // It is quite hard to test it in jsdom environment maybe will be improved in future + /* istanbul ignore next */ + if (conn) { + on(conn, 'change', handleStateChange, { passive: true }); + } - return () => { - off(window, 'online', handleStateChange); - off(window, 'offline', handleStateChange); + return () => { + off(window, 'online', handleStateChange); + off(window, 'offline', handleStateChange); - /* istanbul ignore next */ - if (conn) { - off(conn, 'change', handleStateChange); - } - }; - }, []); + /* istanbul ignore next */ + if (conn) { + off(conn, 'change', handleStateChange); + } + }; + }, []); - return state; + return state; } diff --git a/src/usePermission/__docs__/example.stories.tsx b/src/usePermission/__docs__/example.stories.tsx index 49f0b0099..e6c8c63c3 100644 --- a/src/usePermission/__docs__/example.stories.tsx +++ b/src/usePermission/__docs__/example.stories.tsx @@ -2,32 +2,32 @@ import * as React from 'react'; import { usePermission } from '../..'; export function Example() { - const status = usePermission({ name: 'notifications' }); + const status = usePermission({ name: 'notifications' }); - return ( -
-
- - We do not use any notifications, notifications permission requested only for presentation - purposes. - -
-
-
- Notifications status: {status} -
-
- {status === 'prompt' && ( - - )} -
-
- ); + return ( +
+
+ + We do not use any notifications, notifications permission requested only for presentation + purposes. + +
+
+
+ Notifications status: {status} +
+
+ {status === 'prompt' && ( + + )} +
+
+ ); } diff --git a/src/usePermission/__docs__/story.mdx b/src/usePermission/__docs__/story.mdx index 6e04669c6..14ac52f08 100644 --- a/src/usePermission/__docs__/story.mdx +++ b/src/usePermission/__docs__/story.mdx @@ -14,7 +14,7 @@ Tracks a permission state. #### Example - + ## Reference diff --git a/src/usePermission/__tests__/dom.ts b/src/usePermission/__tests__/dom.ts index 6c04ee937..01a1d9549 100644 --- a/src/usePermission/__tests__/dom.ts +++ b/src/usePermission/__tests__/dom.ts @@ -2,97 +2,97 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { usePermission } from '../..'; describe('usePermission', () => { - let querySpy: jest.SpyInstance; - const initialPermissions = navigator.permissions; - - beforeAll(() => { - jest.useFakeTimers(); - - querySpy = jest.fn( - () => - new Promise((resolve) => { - setTimeout(() => { - resolve({ state: 'prompt' } as PermissionStatus); - }, 1); - }) - ); - - (global.navigator.permissions as any) = { query: querySpy }; - }); - - afterEach(() => { - jest.clearAllTimers(); - querySpy.mockClear(); - }); - - afterAll(() => { - jest.useRealTimers(); - (global.navigator.permissions as any) = initialPermissions; - }); - - it('should be defined', () => { - expect(usePermission).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => usePermission({ name: 'geolocation' })); - expect(result.error).toBeUndefined(); - }); - - it('should have `not-requested` state initially', () => { - const { result } = renderHook(() => usePermission({ name: 'geolocation' })); - expect(result.all[0]).toBe('not-requested'); - }); - - it('should have `requested` state initially', () => { - const { result } = renderHook(() => usePermission({ name: 'geolocation' })); - expect(result.current).toBe('requested'); - }); - - it('should request permission state from `navigator.permissions.query`', () => { - renderHook(() => usePermission({ name: 'geolocation' })); - expect(querySpy).toHaveBeenCalledWith({ name: 'geolocation' }); - }); - - it('should have permission state on promise resolve', async () => { - const { result, waitForNextUpdate } = renderHook(() => usePermission({ name: 'geolocation' })); - - act(() => { - jest.advanceTimersByTime(1); - }); - - await waitForNextUpdate(); - expect(result.current).toBe('prompt'); - }); - - it('should update hook state on permission state change', async () => { - querySpy.mockImplementation( - () => - new Promise((resolve) => { - setTimeout(() => { - const status = { - state: 'prompt', - addEventListener(_n: any, listener: any) { - status.state = 'granted'; - setTimeout(() => listener(), 1); - }, - }; - - resolve(status); - }, 1); - }) - ); - const { result, waitForNextUpdate } = renderHook(() => usePermission({ name: 'geolocation' })); - - act(() => { - jest.advanceTimersByTime(1); - }); - await waitForNextUpdate(); - expect(result.current).toBe('prompt'); - - act(() => { - jest.advanceTimersByTime(1); - }); - expect(result.current).toBe('granted'); - }); + let querySpy: jest.SpyInstance; + const initialPermissions = navigator.permissions; + + beforeAll(() => { + jest.useFakeTimers(); + + querySpy = jest.fn( + () => + new Promise((resolve) => { + setTimeout(() => { + resolve({ state: 'prompt' } as PermissionStatus); + }, 1); + }) + ); + + (global.navigator.permissions as any) = { query: querySpy }; + }); + + afterEach(() => { + jest.clearAllTimers(); + querySpy.mockClear(); + }); + + afterAll(() => { + jest.useRealTimers(); + (global.navigator.permissions as any) = initialPermissions; + }); + + it('should be defined', () => { + expect(usePermission).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => usePermission({ name: 'geolocation' })); + expect(result.error).toBeUndefined(); + }); + + it('should have `not-requested` state initially', () => { + const { result } = renderHook(() => usePermission({ name: 'geolocation' })); + expect(result.all[0]).toBe('not-requested'); + }); + + it('should have `requested` state initially', () => { + const { result } = renderHook(() => usePermission({ name: 'geolocation' })); + expect(result.current).toBe('requested'); + }); + + it('should request permission state from `navigator.permissions.query`', () => { + renderHook(() => usePermission({ name: 'geolocation' })); + expect(querySpy).toHaveBeenCalledWith({ name: 'geolocation' }); + }); + + it('should have permission state on promise resolve', async () => { + const { result, waitForNextUpdate } = renderHook(() => usePermission({ name: 'geolocation' })); + + act(() => { + jest.advanceTimersByTime(1); + }); + + await waitForNextUpdate(); + expect(result.current).toBe('prompt'); + }); + + it('should update hook state on permission state change', async () => { + querySpy.mockImplementation( + () => + new Promise((resolve) => { + setTimeout(() => { + const status = { + state: 'prompt', + addEventListener(_n: any, listener: any) { + status.state = 'granted'; + setTimeout(() => listener(), 1); + }, + }; + + resolve(status); + }, 1); + }) + ); + const { result, waitForNextUpdate } = renderHook(() => usePermission({ name: 'geolocation' })); + + act(() => { + jest.advanceTimersByTime(1); + }); + await waitForNextUpdate(); + expect(result.current).toBe('prompt'); + + act(() => { + jest.advanceTimersByTime(1); + }); + expect(result.current).toBe('granted'); + }); }); diff --git a/src/usePermission/__tests__/ssr.ts b/src/usePermission/__tests__/ssr.ts index 59c9fb4fe..b2bf9962a 100644 --- a/src/usePermission/__tests__/ssr.ts +++ b/src/usePermission/__tests__/ssr.ts @@ -2,12 +2,12 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { usePermission } from '../..'; describe('usePermission', () => { - it('should be defined', () => { - expect(usePermission).toBeDefined(); - }); + it('should be defined', () => { + expect(usePermission).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => usePermission({ name: 'geolocation' })); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => usePermission({ name: 'geolocation' })); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/usePermission/index.ts b/src/usePermission/index.ts index ad98ec946..d152cd7e0 100644 --- a/src/usePermission/index.ts +++ b/src/usePermission/index.ts @@ -9,36 +9,36 @@ export type UsePermissionState = PermissionState | 'not-requested' | 'requested' * @param descriptor Permission request descriptor that passed to `navigator.permissions.query` */ export function usePermission(descriptor: PermissionDescriptor): UsePermissionState { - const [state, setState] = useState('not-requested'); - - useEffect(() => { - const unmount: MutableRefObject<(() => void) | null> = { current: null }; - - setState('requested'); - - // eslint-disable-next-line @typescript-eslint/no-floating-promises,promise/catch-or-return - navigator.permissions - .query(descriptor) - // eslint-disable-next-line promise/always-return - .then((status): void => { - const handleChange = () => { - setState(status.state); - }; - - setState(status.state); - on(status, 'change', handleChange, { passive: true }); - - unmount.current = () => { - off(status, 'change', handleChange); - }; - }); - - return () => { - if (unmount.current) { - unmount.current(); - } - }; - }, [descriptor.name]); - - return state; + const [state, setState] = useState('not-requested'); + + useEffect(() => { + const unmount: MutableRefObject<(() => void) | null> = { current: null }; + + setState('requested'); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises,promise/catch-or-return + navigator.permissions + .query(descriptor) + // eslint-disable-next-line promise/always-return + .then((status): void => { + const handleChange = () => { + setState(status.state); + }; + + setState(status.state); + on(status, 'change', handleChange, { passive: true }); + + unmount.current = () => { + off(status, 'change', handleChange); + }; + }); + + return () => { + if (unmount.current) { + unmount.current(); + } + }; + }, [descriptor.name]); + + return state; } diff --git a/src/usePrevious/__docs__/example.stories.tsx b/src/usePrevious/__docs__/example.stories.tsx index 6573b5f6f..d1b3b6ed6 100644 --- a/src/usePrevious/__docs__/example.stories.tsx +++ b/src/usePrevious/__docs__/example.stories.tsx @@ -2,42 +2,42 @@ import React, { useState } from 'react'; import { usePrevious } from '../..'; export function Example() { - const [value, setValue] = useState(0); - const [unrelatedValue, setUnrelatedValue] = useState(0); - const previousValue = usePrevious(value); - - const increment = () => { - setValue((v) => v + 1); - }; - - const decrement = () => { - setValue((v) => v - 1); - }; - - const triggerUnrelatedRerender = () => { - setUnrelatedValue((v) => v + 1); - }; - - return ( -
- Current value: {value} - -
-
- - -
- - -
- -
Previous value: "{previousValue ?? 'undefined'}"
-
- ); + const [value, setValue] = useState(0); + const [unrelatedValue, setUnrelatedValue] = useState(0); + const previousValue = usePrevious(value); + + const increment = () => { + setValue((v) => v + 1); + }; + + const decrement = () => { + setValue((v) => v - 1); + }; + + const triggerUnrelatedRerender = () => { + setUnrelatedValue((v) => v + 1); + }; + + return ( +
+ Current value: {value} + +
+
+ + +
+ + +
+ +
Previous value: "{previousValue ?? 'undefined'}"
+
+ ); } diff --git a/src/usePrevious/__docs__/story.mdx b/src/usePrevious/__docs__/story.mdx index 709861290..7f71acb5a 100644 --- a/src/usePrevious/__docs__/story.mdx +++ b/src/usePrevious/__docs__/story.mdx @@ -14,7 +14,7 @@ however, if your desire is to track distinctly different values, you should use #### Example - + ## Reference diff --git a/src/usePrevious/__tests__/dom.ts b/src/usePrevious/__tests__/dom.ts index cd4dc3873..4b404bd46 100644 --- a/src/usePrevious/__tests__/dom.ts +++ b/src/usePrevious/__tests__/dom.ts @@ -2,44 +2,44 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { usePrevious } from '../..'; describe('usePrevious', () => { - it('should be defined', () => { - expect(usePrevious).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => usePrevious()); - expect(result.error).toBeUndefined(); - }); - - it('should return undefined on first render', () => { - const { result } = renderHook(() => usePrevious()); - - expect(result.current).toBeUndefined(); - }); - - it('should return previously passed value on rerender', () => { - const { result, rerender } = renderHook(({ state }) => usePrevious(state), { - initialProps: { state: 0 }, - }); - - expect(result.current).toBeUndefined(); - rerender({ state: 1 }); - expect(result.current).toBe(0); - rerender({ state: 5 }); - expect(result.current).toBe(1); - rerender({ state: 10 }); - expect(result.current).toBe(5); - rerender({ state: 25 }); - expect(result.current).toBe(10); - }); - - it('should return passed value after unrelated rerender', () => { - const { result, rerender } = renderHook(({ state }) => usePrevious(state), { - initialProps: { state: 0 }, - }); - - expect(result.current).toBeUndefined(); - rerender(); - expect(result.current).toBe(0); - }); + it('should be defined', () => { + expect(usePrevious).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => usePrevious()); + expect(result.error).toBeUndefined(); + }); + + it('should return undefined on first render', () => { + const { result } = renderHook(() => usePrevious()); + + expect(result.current).toBeUndefined(); + }); + + it('should return previously passed value on rerender', () => { + const { result, rerender } = renderHook(({ state }) => usePrevious(state), { + initialProps: { state: 0 }, + }); + + expect(result.current).toBeUndefined(); + rerender({ state: 1 }); + expect(result.current).toBe(0); + rerender({ state: 5 }); + expect(result.current).toBe(1); + rerender({ state: 10 }); + expect(result.current).toBe(5); + rerender({ state: 25 }); + expect(result.current).toBe(10); + }); + + it('should return passed value after unrelated rerender', () => { + const { result, rerender } = renderHook(({ state }) => usePrevious(state), { + initialProps: { state: 0 }, + }); + + expect(result.current).toBeUndefined(); + rerender(); + expect(result.current).toBe(0); + }); }); diff --git a/src/usePrevious/__tests__/ssr.ts b/src/usePrevious/__tests__/ssr.ts index 502fd54ee..655bbf7c1 100644 --- a/src/usePrevious/__tests__/ssr.ts +++ b/src/usePrevious/__tests__/ssr.ts @@ -2,18 +2,18 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { usePrevious } from '../..'; describe('usePrevious', () => { - it('should be defined', () => { - expect(usePrevious).toBeDefined(); - }); + it('should be defined', () => { + expect(usePrevious).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => usePrevious()); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => usePrevious()); + expect(result.error).toBeUndefined(); + }); - it('should return undefined on first render', () => { - const { result } = renderHook(() => usePrevious()); + it('should return undefined on first render', () => { + const { result } = renderHook(() => usePrevious()); - expect(result.current).toBeUndefined(); - }); + expect(result.current).toBeUndefined(); + }); }); diff --git a/src/usePrevious/index.ts b/src/usePrevious/index.ts index b71d7889b..a6a2fa870 100644 --- a/src/usePrevious/index.ts +++ b/src/usePrevious/index.ts @@ -8,11 +8,11 @@ import { useEffect, useRef } from 'react'; * @param value Value to yield on next render */ export function usePrevious(value?: T): T | undefined { - const prev = useRef(); + const prev = useRef(); - useEffect(() => { - prev.current = value; - }); + useEffect(() => { + prev.current = value; + }); - return prev.current; + return prev.current; } diff --git a/src/usePreviousDistinct/__docs__/example.stories.tsx b/src/usePreviousDistinct/__docs__/example.stories.tsx index d14af3ecd..a11642976 100644 --- a/src/usePreviousDistinct/__docs__/example.stories.tsx +++ b/src/usePreviousDistinct/__docs__/example.stories.tsx @@ -2,42 +2,42 @@ import React, { useState } from 'react'; import { usePreviousDistinct } from '../..'; export function Example() { - const [value, setValue] = useState(0); - const [unrelatedValue, setUnrelatedValue] = useState(0); - const previousDistinctValue = usePreviousDistinct(value); - - const increment = () => { - setValue((v) => v + 1); - }; - - const decrement = () => { - setValue((v) => v - 1); - }; - - const triggerUnrelatedRerender = () => { - setUnrelatedValue((v) => v + 1); - }; - - return ( -
- Current value: {value} - -
-
- - -
- - -
- -
Previous value: "{previousDistinctValue ?? 'undefined'}"
-
- ); + const [value, setValue] = useState(0); + const [unrelatedValue, setUnrelatedValue] = useState(0); + const previousDistinctValue = usePreviousDistinct(value); + + const increment = () => { + setValue((v) => v + 1); + }; + + const decrement = () => { + setValue((v) => v - 1); + }; + + const triggerUnrelatedRerender = () => { + setUnrelatedValue((v) => v + 1); + }; + + return ( +
+ Current value: {value} + +
+
+ + +
+ + +
+ +
Previous value: "{previousDistinctValue ?? 'undefined'}"
+
+ ); } diff --git a/src/usePreviousDistinct/__docs__/story.mdx b/src/usePreviousDistinct/__docs__/story.mdx index 5756cdb06..b3d00bafc 100644 --- a/src/usePreviousDistinct/__docs__/story.mdx +++ b/src/usePreviousDistinct/__docs__/story.mdx @@ -13,7 +13,7 @@ other renders are involved potentially making multiple, irrelevant updates. #### Example - + ## Reference @@ -24,8 +24,8 @@ export type Predicate = (prev: T, next: any) => next is typeof prev; const isStrictEqual = (a: T, b: any): b is typeof a => a === b; export function usePreviousDistinct( - value: T, - predicate: Predicate = isStrictEqual + value: T, + predicate: Predicate = isStrictEqual ): T | undefined; ``` diff --git a/src/usePreviousDistinct/__tests__/dom.ts b/src/usePreviousDistinct/__tests__/dom.ts index 62a2a7e49..f4e7c745e 100644 --- a/src/usePreviousDistinct/__tests__/dom.ts +++ b/src/usePreviousDistinct/__tests__/dom.ts @@ -3,96 +3,96 @@ import { usePreviousDistinct } from '../..'; import { isStrictEqual } from '../../util/const'; describe('usePreviousDistinct', () => { - it('should be defined', () => { - expect(usePreviousDistinct).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => usePreviousDistinct(0)); - expect(result.error).toBeUndefined(); - }); - - it('should return undefined on first render', () => { - const { result } = renderHook(() => usePreviousDistinct(0)); - expect(result.current).toBeUndefined(); - }); - - it('should return undefined on first render with compare function passed', () => { - const { result } = renderHook(() => usePreviousDistinct(0, isStrictEqual)); - expect(result.current).toBeUndefined(); - }); - - it('should not invoke predicate on first render', () => { - const mockedCompare = jest.fn(); - - const { result } = renderHook(() => usePreviousDistinct(0, mockedCompare as any)); - expect(result.current).toBeUndefined(); - expect(mockedCompare).not.toHaveBeenCalled(); - }); - - it('should not return passed value after unrelated rerender', () => { - const { result, rerender } = renderHook(({ state }) => usePreviousDistinct(state), { - initialProps: { state: 0 }, - }); - - expect(result.current).toBeUndefined(); - rerender(); - expect(result.current).not.toBe(0); - expect(result.current).toBeUndefined(); - }); - - it('should return passed value after related rerender', () => { - const { result, rerender } = renderHook(({ state }) => usePreviousDistinct(state), { - initialProps: { state: 0 }, - }); - - expect(result.current).toBeUndefined(); // Asserting against initial render. - rerender({ state: 1 }); - expect(result.current).toBe(0); // Asserting against first re-render. value has now changed - }); - - it('should update previous value only after render with different value', () => { - const { result, rerender } = renderHook(({ state }) => usePreviousDistinct(state), { - initialProps: { state: 0 }, - }); - - expect(result.current).toBeUndefined(); - rerender({ state: 1 }); // Update - expect(result.current).toBe(0); - rerender({ state: 5 }); // Update - expect(result.current).toBe(1); - rerender({ state: 5 }); // No update - expect(result.current).toBe(1); - }); - - it('should not update to value if it never changes, depsite rerenders', () => { - const value = 'yo'; - const { result, rerender } = renderHook(({ state }) => usePreviousDistinct(state), { - initialProps: { state: value }, - }); - - expect(result.current).toBeUndefined(); - rerender({ state: value }); - expect(result.current).toBeUndefined(); - rerender({ state: value }); - expect(result.current).toBeUndefined(); - rerender({ state: value }); - }); - - it('should update even when going between defined and undefined values', () => { - const { result, rerender } = renderHook<{ state: number | undefined }, number | undefined>( - ({ state }: { state: number | undefined }) => usePreviousDistinct(state), - { - initialProps: { state: 0 }, - } - ); - - expect(result.current).toBeUndefined(); - rerender({ state: 1 }); - expect(result.current).toBe(0); - rerender({ state: undefined }); - expect(result.current).toBe(1); - rerender({ state: 10 }); - expect(result.current).toBeUndefined(); - }); + it('should be defined', () => { + expect(usePreviousDistinct).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => usePreviousDistinct(0)); + expect(result.error).toBeUndefined(); + }); + + it('should return undefined on first render', () => { + const { result } = renderHook(() => usePreviousDistinct(0)); + expect(result.current).toBeUndefined(); + }); + + it('should return undefined on first render with compare function passed', () => { + const { result } = renderHook(() => usePreviousDistinct(0, isStrictEqual)); + expect(result.current).toBeUndefined(); + }); + + it('should not invoke predicate on first render', () => { + const mockedCompare = jest.fn(); + + const { result } = renderHook(() => usePreviousDistinct(0, mockedCompare as any)); + expect(result.current).toBeUndefined(); + expect(mockedCompare).not.toHaveBeenCalled(); + }); + + it('should not return passed value after unrelated rerender', () => { + const { result, rerender } = renderHook(({ state }) => usePreviousDistinct(state), { + initialProps: { state: 0 }, + }); + + expect(result.current).toBeUndefined(); + rerender(); + expect(result.current).not.toBe(0); + expect(result.current).toBeUndefined(); + }); + + it('should return passed value after related rerender', () => { + const { result, rerender } = renderHook(({ state }) => usePreviousDistinct(state), { + initialProps: { state: 0 }, + }); + + expect(result.current).toBeUndefined(); // Asserting against initial render. + rerender({ state: 1 }); + expect(result.current).toBe(0); // Asserting against first re-render. value has now changed + }); + + it('should update previous value only after render with different value', () => { + const { result, rerender } = renderHook(({ state }) => usePreviousDistinct(state), { + initialProps: { state: 0 }, + }); + + expect(result.current).toBeUndefined(); + rerender({ state: 1 }); // Update + expect(result.current).toBe(0); + rerender({ state: 5 }); // Update + expect(result.current).toBe(1); + rerender({ state: 5 }); // No update + expect(result.current).toBe(1); + }); + + it('should not update to value if it never changes, depsite rerenders', () => { + const value = 'yo'; + const { result, rerender } = renderHook(({ state }) => usePreviousDistinct(state), { + initialProps: { state: value }, + }); + + expect(result.current).toBeUndefined(); + rerender({ state: value }); + expect(result.current).toBeUndefined(); + rerender({ state: value }); + expect(result.current).toBeUndefined(); + rerender({ state: value }); + }); + + it('should update even when going between defined and undefined values', () => { + const { result, rerender } = renderHook<{ state: number | undefined }, number | undefined>( + ({ state }: { state: number | undefined }) => usePreviousDistinct(state), + { + initialProps: { state: 0 }, + } + ); + + expect(result.current).toBeUndefined(); + rerender({ state: 1 }); + expect(result.current).toBe(0); + rerender({ state: undefined }); + expect(result.current).toBe(1); + rerender({ state: 10 }); + expect(result.current).toBeUndefined(); + }); }); diff --git a/src/usePreviousDistinct/__tests__/ssr.ts b/src/usePreviousDistinct/__tests__/ssr.ts index 599e7edfd..1cd35302c 100644 --- a/src/usePreviousDistinct/__tests__/ssr.ts +++ b/src/usePreviousDistinct/__tests__/ssr.ts @@ -3,24 +3,24 @@ import { usePreviousDistinct } from '../..'; import { isStrictEqual } from '../../util/const'; describe('usePreviousDistinct', () => { - it('should be defined', () => { - expect(usePreviousDistinct).toBeDefined(); - }); + it('should be defined', () => { + expect(usePreviousDistinct).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => usePreviousDistinct(0)); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => usePreviousDistinct(0)); + expect(result.error).toBeUndefined(); + }); - it('should return undefined on first render', () => { - const { result } = renderHook(() => usePreviousDistinct(0)); + it('should return undefined on first render', () => { + const { result } = renderHook(() => usePreviousDistinct(0)); - expect(result.current).toBeUndefined(); - }); + expect(result.current).toBeUndefined(); + }); - it('should return undefined on first render with compare function passed', () => { - const { result } = renderHook(() => usePreviousDistinct(0, isStrictEqual)); + it('should return undefined on first render with compare function passed', () => { + const { result } = renderHook(() => usePreviousDistinct(0, isStrictEqual)); - expect(result.current).toBeUndefined(); - }); + expect(result.current).toBeUndefined(); + }); }); diff --git a/src/usePreviousDistinct/index.ts b/src/usePreviousDistinct/index.ts index d456c2f23..53d6d57b2 100644 --- a/src/usePreviousDistinct/index.ts +++ b/src/usePreviousDistinct/index.ts @@ -14,18 +14,18 @@ import type { Predicate } from '../types'; * the value will be updated if it is strictly equal (`===`) to the previous value. */ export function usePreviousDistinct( - value: T, - predicate: Predicate = isStrictEqual + value: T, + predicate: Predicate = isStrictEqual ): T | undefined { - const [previousState, setPreviousState] = useState(); - const currentRef = useRef(value); + const [previousState, setPreviousState] = useState(); + const currentRef = useRef(value); - useUpdateEffect(() => { - if (!predicate(currentRef.current, value)) { - setPreviousState(currentRef.current); - currentRef.current = value; - } - }, [value]); + useUpdateEffect(() => { + if (!predicate(currentRef.current, value)) { + setPreviousState(currentRef.current); + currentRef.current = value; + } + }, [value]); - return previousState; + return previousState; } diff --git a/src/useQueue/__docs__/example.stories.tsx b/src/useQueue/__docs__/example.stories.tsx index ee7fafc65..b872f528b 100644 --- a/src/useQueue/__docs__/example.stories.tsx +++ b/src/useQueue/__docs__/example.stories.tsx @@ -2,32 +2,32 @@ import * as React from 'react'; import { useQueue } from '../..'; export function Example() { - const { add, remove, first, last, size, items } = useQueue([1, 2, 3]); + const { add, remove, first, last, size, items } = useQueue([1, 2, 3]); - return ( -
-
    -
  • first: {first}
  • -
  • last: {last}
  • -
  • size: {size}
  • -
- - -

All Items

-
    - {items.map((item, idx) => ( - // eslint-disable-next-line react/no-array-index-key -
  • {item}
  • - ))} -
-
- ); + return ( +
+
    +
  • first: {first}
  • +
  • last: {last}
  • +
  • size: {size}
  • +
+ + +

All Items

+
    + {items.map((item, idx) => ( + // eslint-disable-next-line react/no-array-index-key +
  • {item}
  • + ))} +
+
+ ); } diff --git a/src/useQueue/__docs__/story.mdx b/src/useQueue/__docs__/story.mdx index 7c5b897d3..8c33a200a 100644 --- a/src/useQueue/__docs__/story.mdx +++ b/src/useQueue/__docs__/story.mdx @@ -11,19 +11,19 @@ A state hook implementing FIFO queue. #### Example - + ## Reference ```ts export interface QueueMethods { - first: T; - last: T; - add: (item: T) => void; - remove: () => T; - size: number; - items: T[]; + first: T; + last: T; + add: (item: T) => void; + remove: () => T; + size: number; + items: T[]; } export function useQueue(initialValue: T[] = []): QueueMethods; ``` diff --git a/src/useQueue/__tests__/dom.ts b/src/useQueue/__tests__/dom.ts index a1be406d0..86f8336a4 100644 --- a/src/useQueue/__tests__/dom.ts +++ b/src/useQueue/__tests__/dom.ts @@ -2,58 +2,58 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { useQueue } from '../..'; describe('useQueue', () => { - it('should be defined', () => { - expect(useQueue).toBeDefined(); - }); + it('should be defined', () => { + expect(useQueue).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useQueue()); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useQueue()); + expect(result.error).toBeUndefined(); + }); - it('should accept an initial value', () => { - const { result } = renderHook(() => useQueue([0, 1, 2, 3])); - expect(result.current.items).toStrictEqual([0, 1, 2, 3]); - }); + it('should accept an initial value', () => { + const { result } = renderHook(() => useQueue([0, 1, 2, 3])); + expect(result.current.items).toStrictEqual([0, 1, 2, 3]); + }); - it('should remove the first value', () => { - const { result } = renderHook(() => useQueue([0, 1, 2, 3])); + it('should remove the first value', () => { + const { result } = renderHook(() => useQueue([0, 1, 2, 3])); - act(() => { - const removed = result.current.remove(); - expect(removed).toBe(0); - }); + act(() => { + const removed = result.current.remove(); + expect(removed).toBe(0); + }); - expect(result.current.first).toBe(1); - }); + expect(result.current.first).toBe(1); + }); - it('should return the length', () => { - const { result } = renderHook(() => useQueue([0, 1, 2, 3])); - expect(result.current.size).toBe(4); - }); + it('should return the length', () => { + const { result } = renderHook(() => useQueue([0, 1, 2, 3])); + expect(result.current.size).toBe(4); + }); - it('should add a value to the end', () => { - const { result } = renderHook(() => useQueue([0, 1, 2, 3])); + it('should add a value to the end', () => { + const { result } = renderHook(() => useQueue([0, 1, 2, 3])); - act(() => { - result.current.add(4); - }); + act(() => { + result.current.add(4); + }); - expect(result.current.last).toBe(4); - }); + expect(result.current.last).toBe(4); + }); - it('should return referentially stable functions', () => { - const { result } = renderHook(() => useQueue([0, 1, 2, 3])); + it('should return referentially stable functions', () => { + const { result } = renderHook(() => useQueue([0, 1, 2, 3])); - const remove1 = result.current.remove; - const add1 = result.current.add; + const remove1 = result.current.remove; + const add1 = result.current.add; - act(() => { - result.current.add(1); - result.current.remove(); - }); + act(() => { + result.current.add(1); + result.current.remove(); + }); - expect(result.current.remove).toBe(remove1); - expect(result.current.add).toBe(add1); - }); + expect(result.current.remove).toBe(remove1); + expect(result.current.add).toBe(add1); + }); }); diff --git a/src/useQueue/__tests__/ssr.ts b/src/useQueue/__tests__/ssr.ts index 09c74492f..2c2aa9d46 100644 --- a/src/useQueue/__tests__/ssr.ts +++ b/src/useQueue/__tests__/ssr.ts @@ -2,17 +2,17 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useQueue } from '../..'; describe('useQueue', () => { - it('should be defined', () => { - expect(useQueue).toBeDefined(); - }); + it('should be defined', () => { + expect(useQueue).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useQueue()); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useQueue()); + expect(result.error).toBeUndefined(); + }); - it('should return an object', () => { - const { result } = renderHook(() => useQueue()); - expect(result.current).toBeInstanceOf(Object); - }); + it('should return an object', () => { + const { result } = renderHook(() => useQueue()); + expect(result.current).toBeInstanceOf(Object); + }); }); diff --git a/src/useQueue/index.ts b/src/useQueue/index.ts index 0e2ad523c..3c572a9c6 100644 --- a/src/useQueue/index.ts +++ b/src/useQueue/index.ts @@ -3,31 +3,31 @@ import { useList } from '../useList'; import { useSyncedRef } from '../useSyncedRef'; export type QueueMethods = { - /** - * The entire queue. - */ - items: T[]; - /** - * The first item in the queue. - */ - first: T; - /** - * The last item in the queue. - */ - last: T; - /** - * Adds an item to the end of the queue. - * @param item The item to be added. - */ - add: (item: T) => void; - /** - * Removes and returns the head of the queue. - */ - remove: () => T; - /** - * The current size of the queue. - */ - size: number; + /** + * The entire queue. + */ + items: T[]; + /** + * The first item in the queue. + */ + first: T; + /** + * The last item in the queue. + */ + last: T; + /** + * Adds an item to the end of the queue. + * @param item The item to be added. + */ + add: (item: T) => void; + /** + * Removes and returns the head of the queue. + */ + remove: () => T; + /** + * The current size of the queue. + */ + size: number; }; /** @@ -36,35 +36,35 @@ export type QueueMethods = { * @param initialValue The initial value. Defaults to an empty array. */ export function useQueue(initialValue: T[] = []): QueueMethods { - const [list, { removeAt, push }] = useList(initialValue); - const listRef = useSyncedRef(list); + const [list, { removeAt, push }] = useList(initialValue); + const listRef = useSyncedRef(list); - return useMemo( - () => ({ - add(value: T) { - push(value); - }, - remove() { - const val = listRef.current[0]; + return useMemo( + () => ({ + add(value: T) { + push(value); + }, + remove() { + const val = listRef.current[0]; - removeAt(0); + removeAt(0); - return val; - }, - get first() { - return listRef.current[0]; - }, - get last() { - return listRef.current[listRef.current.length - 1]; - }, - get size() { - return listRef.current.length; - }, - get items() { - return listRef.current; - }, - }), + return val; + }, + get first() { + return listRef.current[0]; + }, + get last() { + return listRef.current[listRef.current.length - 1]; + }, + get size() { + return listRef.current.length; + }, + get items() { + return listRef.current; + }, + }), - [] - ); + [] + ); } diff --git a/src/useRafCallback/__docs__/example.stories.tsx b/src/useRafCallback/__docs__/example.stories.tsx index 3103006c5..385802bc4 100644 --- a/src/useRafCallback/__docs__/example.stories.tsx +++ b/src/useRafCallback/__docs__/example.stories.tsx @@ -2,31 +2,31 @@ import React, { useCallback, useState } from 'react'; import { useRafCallback } from '../..'; export function Example() { - const [eventDate, setEventDate] = useState(); - const [frameDate, setFrameDate] = useState(); + const [eventDate, setEventDate] = useState(); + const [frameDate, setFrameDate] = useState(); - const [storeFrameDate] = useRafCallback(() => { - setFrameDate(new Date()); - }); + const [storeFrameDate] = useRafCallback(() => { + setFrameDate(new Date()); + }); - const handleClick = useCallback(() => { - setEventDate(new Date()); - storeFrameDate(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const handleClick = useCallback(() => { + setEventDate(new Date()); + storeFrameDate(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - return ( -
-
- Below states displays dates when occurred event and animation frame that following it -
-
-
Event occurred: {eventDate?.toISOString() ?? 'NOT TRIGGERED'}
-
Animation frame occurred: {frameDate?.toISOString()}
-
- -
- ); + return ( +
+
+ Below states displays dates when occurred event and animation frame that following it +
+
+
Event occurred: {eventDate?.toISOString() ?? 'NOT TRIGGERED'}
+
Animation frame occurred: {frameDate?.toISOString()}
+
+ +
+ ); } diff --git a/src/useRafCallback/__docs__/story.mdx b/src/useRafCallback/__docs__/story.mdx index e3a234248..8b474128f 100644 --- a/src/useRafCallback/__docs__/story.mdx +++ b/src/useRafCallback/__docs__/story.mdx @@ -17,14 +17,14 @@ Consequential calls, before the animation frame occurred, cancel previously sche #### Example - + ## Reference ```ts export function useRafCallback any>( - cb: T + cb: T ): [(...args: Parameters) => void, () => void]; ``` diff --git a/src/useRafCallback/__tests__/dom.ts b/src/useRafCallback/__tests__/dom.ts index 22156f6bb..0946f9e4d 100644 --- a/src/useRafCallback/__tests__/dom.ts +++ b/src/useRafCallback/__tests__/dom.ts @@ -2,112 +2,112 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useRafCallback } from '../..'; describe('useRafCallback', () => { - const raf = global.requestAnimationFrame; - const caf = global.cancelAnimationFrame; + const raf = global.requestAnimationFrame; + const caf = global.cancelAnimationFrame; - beforeAll(() => { - jest.useFakeTimers(); + beforeAll(() => { + jest.useFakeTimers(); - global.requestAnimationFrame = (cb) => setTimeout(cb); - global.cancelAnimationFrame = (cb) => { - clearTimeout(cb); - }; - }); + global.requestAnimationFrame = (cb) => setTimeout(cb); + global.cancelAnimationFrame = (cb) => { + clearTimeout(cb); + }; + }); - afterEach(() => { - jest.clearAllTimers(); - }); + afterEach(() => { + jest.clearAllTimers(); + }); - afterAll(() => { - jest.useRealTimers(); + afterAll(() => { + jest.useRealTimers(); - global.requestAnimationFrame = raf; - global.cancelAnimationFrame = caf; - }); + global.requestAnimationFrame = raf; + global.cancelAnimationFrame = caf; + }); - it('should be defined', () => { - expect(useRafCallback).toBeDefined(); - }); + it('should be defined', () => { + expect(useRafCallback).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useRafCallback(() => {})); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useRafCallback(() => {})); + expect(result.error).toBeUndefined(); + }); - it('should return function same length and wrapped name', () => { - let { result } = renderHook(() => useRafCallback((_a: any, _b: any, _c: any) => {})); + it('should return function same length and wrapped name', () => { + let { result } = renderHook(() => useRafCallback((_a: any, _b: any, _c: any) => {})); - expect(result.current[0].length).toBe(3); - expect(result.current[0].name).toBe(`anonymous__raf`); + expect(result.current[0].length).toBe(3); + expect(result.current[0].name).toBe(`anonymous__raf`); - function testFn(_a: any, _b: any, _c: any) {} + function testFn(_a: any, _b: any, _c: any) {} - result = renderHook(() => useRafCallback(testFn)).result; + result = renderHook(() => useRafCallback(testFn)).result; - expect(result.current[0].length).toBe(3); - expect(result.current[0].name).toBe(`testFn__raf`); - }); + expect(result.current[0].length).toBe(3); + expect(result.current[0].name).toBe(`testFn__raf`); + }); - it('should return array of functions', () => { - const { result } = renderHook(() => useRafCallback(() => {})); + it('should return array of functions', () => { + const { result } = renderHook(() => useRafCallback(() => {})); - expect(result.current).toBeInstanceOf(Array); - expect(result.current[0]).toBeInstanceOf(Function); - expect(result.current[1]).toBeInstanceOf(Function); - }); + expect(result.current).toBeInstanceOf(Array); + expect(result.current[0]).toBeInstanceOf(Function); + expect(result.current[1]).toBeInstanceOf(Function); + }); - it('should invoke passed function only on next raf', () => { - const spy = jest.fn(); - const { result } = renderHook(() => useRafCallback(spy)); + it('should invoke passed function only on next raf', () => { + const spy = jest.fn(); + const { result } = renderHook(() => useRafCallback(spy)); - result.current[0](); + result.current[0](); - expect(spy).not.toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); - jest.advanceTimersToNextTimer(); + jest.advanceTimersToNextTimer(); - expect(spy).toHaveBeenCalled(); - }); + expect(spy).toHaveBeenCalled(); + }); - it('should auto-cancel scheduled invocation on consequential calls', () => { - const spy = jest.fn(); - const { result } = renderHook(() => useRafCallback(spy)); + it('should auto-cancel scheduled invocation on consequential calls', () => { + const spy = jest.fn(); + const { result } = renderHook(() => useRafCallback(spy)); - result.current[0](); - result.current[0](); - result.current[0](); - result.current[0](); + result.current[0](); + result.current[0](); + result.current[0](); + result.current[0](); - expect(spy).not.toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); - jest.advanceTimersToNextTimer(5); + jest.advanceTimersToNextTimer(5); - expect(spy).toHaveBeenCalledTimes(1); - }); + expect(spy).toHaveBeenCalledTimes(1); + }); - it('should cancel scheduled invocation on second function call', () => { - const spy = jest.fn(); - const { result } = renderHook(() => useRafCallback(spy)); + it('should cancel scheduled invocation on second function call', () => { + const spy = jest.fn(); + const { result } = renderHook(() => useRafCallback(spy)); - result.current[0](); + result.current[0](); - result.current[1](); + result.current[1](); - jest.advanceTimersToNextTimer(5); + jest.advanceTimersToNextTimer(5); - expect(spy).not.toHaveBeenCalled(); - }); + expect(spy).not.toHaveBeenCalled(); + }); - it('should auto-cancel scheduled invocation on component unmount', () => { - const spy = jest.fn(); - const { result, unmount } = renderHook(() => useRafCallback(spy)); + it('should auto-cancel scheduled invocation on component unmount', () => { + const spy = jest.fn(); + const { result, unmount } = renderHook(() => useRafCallback(spy)); - result.current[0](); + result.current[0](); - unmount(); + unmount(); - jest.advanceTimersToNextTimer(5); + jest.advanceTimersToNextTimer(5); - expect(spy).not.toHaveBeenCalled(); - }); + expect(spy).not.toHaveBeenCalled(); + }); }); diff --git a/src/useRafCallback/__tests__/ssr.ts b/src/useRafCallback/__tests__/ssr.ts index 048e07337..75d86373c 100644 --- a/src/useRafCallback/__tests__/ssr.ts +++ b/src/useRafCallback/__tests__/ssr.ts @@ -2,30 +2,30 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useRafCallback } from '../..'; describe('useRafCallback', () => { - it('should be defined', () => { - expect(useRafCallback).toBeDefined(); - }); + it('should be defined', () => { + expect(useRafCallback).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useRafCallback(() => {})); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useRafCallback(() => {})); + expect(result.error).toBeUndefined(); + }); - it('should return array of functions', () => { - const { result } = renderHook(() => useRafCallback(() => {})); + it('should return array of functions', () => { + const { result } = renderHook(() => useRafCallback(() => {})); - expect(result.current).toBeInstanceOf(Array); - expect(result.current[0]).toBeInstanceOf(Function); - expect(result.current[1]).toBeInstanceOf(Function); - }); + expect(result.current).toBeInstanceOf(Array); + expect(result.current[0]).toBeInstanceOf(Function); + expect(result.current[1]).toBeInstanceOf(Function); + }); - it('should not do anything on returned functions invocation', () => { - const spy = jest.fn(); - const { result } = renderHook(() => useRafCallback(spy)); + it('should not do anything on returned functions invocation', () => { + const spy = jest.fn(); + const { result } = renderHook(() => useRafCallback(spy)); - result.current[0](); - result.current[1](); + result.current[0](); + result.current[1](); - expect(spy).not.toHaveBeenCalled(); - }); + expect(spy).not.toHaveBeenCalled(); + }); }); diff --git a/src/useRafCallback/index.ts b/src/useRafCallback/index.ts index d18392161..3cad51f0a 100644 --- a/src/useRafCallback/index.ts +++ b/src/useRafCallback/index.ts @@ -12,43 +12,43 @@ import { isBrowser } from '../util/const'; */ export function useRafCallback any>( - cb: T + cb: T ): [(...args: Parameters) => void, () => void] { - const cbRef = useSyncedRef(cb); - const frame = useRef(0); + const cbRef = useSyncedRef(cb); + const frame = useRef(0); - const cancel = useCallback(() => { - if (!isBrowser) return; + const cancel = useCallback(() => { + if (!isBrowser) return; - if (frame.current) { - cancelAnimationFrame(frame.current); - frame.current = 0; - } - }, []); + if (frame.current) { + cancelAnimationFrame(frame.current); + frame.current = 0; + } + }, []); - useUnmountEffect(cancel); + useUnmountEffect(cancel); - return [ - useMemo(() => { - const wrapped = (...args: Parameters) => { - if (!isBrowser) return; + return [ + useMemo(() => { + const wrapped = (...args: Parameters) => { + if (!isBrowser) return; - cancel(); + cancel(); - frame.current = requestAnimationFrame(() => { - cbRef.current(...args); - frame.current = 0; - }); - }; + frame.current = requestAnimationFrame(() => { + cbRef.current(...args); + frame.current = 0; + }); + }; - Object.defineProperties(wrapped, { - length: { value: cb.length }, - name: { value: `${cb.name || 'anonymous'}__raf` }, - }); + Object.defineProperties(wrapped, { + length: { value: cb.length }, + name: { value: `${cb.name || 'anonymous'}__raf` }, + }); - return wrapped; - }, []), + return wrapped; + }, []), - cancel, - ]; + cancel, + ]; } diff --git a/src/useRafEffect/__docs__/example.stories.tsx b/src/useRafEffect/__docs__/example.stories.tsx index f7fdbacf0..72a2d6d47 100644 --- a/src/useRafEffect/__docs__/example.stories.tsx +++ b/src/useRafEffect/__docs__/example.stories.tsx @@ -3,26 +3,26 @@ import { useRef } from 'react'; import { useRafEffect } from '../..'; export function Example() { - const inputRef = useRef(null); + const inputRef = useRef(null); - useRafEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, [inputRef.current]); + useRafEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, [inputRef.current]); - return ( -
- ); + return ( +
+ The focus will be set on the input element below after the first animation frame. This is + helpful if your input is, for example, in a portal and you encouter issues interacting with + the DOM before the first animation frame.{' '} + + See this issue + + . +
+ +
+
+ ); } diff --git a/src/useRafEffect/__docs__/story.mdx b/src/useRafEffect/__docs__/story.mdx index baa97a2f7..02336bfc9 100644 --- a/src/useRafEffect/__docs__/story.mdx +++ b/src/useRafEffect/__docs__/story.mdx @@ -14,7 +14,7 @@ Like `React.useEffect`, but effect is only run within animation frame. #### Example - + ## Reference diff --git a/src/useRafEffect/__tests__/dom.ts b/src/useRafEffect/__tests__/dom.ts index e173cb5f3..f67aba514 100644 --- a/src/useRafEffect/__tests__/dom.ts +++ b/src/useRafEffect/__tests__/dom.ts @@ -2,87 +2,87 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { useRafEffect } from '../..'; describe('useRafEffect', () => { - const raf = global.requestAnimationFrame; - const caf = global.cancelAnimationFrame; + const raf = global.requestAnimationFrame; + const caf = global.cancelAnimationFrame; - beforeAll(() => { - jest.useFakeTimers(); + beforeAll(() => { + jest.useFakeTimers(); - global.requestAnimationFrame = (cb) => setTimeout(cb); - global.cancelAnimationFrame = (cb) => { - clearTimeout(cb); - }; - }); + global.requestAnimationFrame = (cb) => setTimeout(cb); + global.cancelAnimationFrame = (cb) => { + clearTimeout(cb); + }; + }); - afterEach(() => { - jest.clearAllTimers(); - }); + afterEach(() => { + jest.clearAllTimers(); + }); - afterAll(() => { - jest.useRealTimers(); + afterAll(() => { + jest.useRealTimers(); - global.requestAnimationFrame = raf; - global.cancelAnimationFrame = caf; - }); + global.requestAnimationFrame = raf; + global.cancelAnimationFrame = caf; + }); - it('should be defined', () => { - expect(useRafEffect).toBeDefined(); - }); + it('should be defined', () => { + expect(useRafEffect).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useRafEffect(() => {}, []); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useRafEffect(() => {}, []); + }); + expect(result.error).toBeUndefined(); + }); - it('should not run unless animation frame', () => { - const spy = jest.fn(); - const { rerender } = renderHook( - (dep) => { - useRafEffect(spy, [dep]); - }, - { - initialProps: 1, - } - ); + it('should not run unless animation frame', () => { + const spy = jest.fn(); + const { rerender } = renderHook( + (dep) => { + useRafEffect(spy, [dep]); + }, + { + initialProps: 1, + } + ); - expect(spy).toHaveBeenCalledTimes(0); + expect(spy).toHaveBeenCalledTimes(0); - rerender(2); + rerender(2); - expect(spy).toHaveBeenCalledTimes(0); + expect(spy).toHaveBeenCalledTimes(0); - act(() => { - jest.advanceTimersToNextTimer(); - }); + act(() => { + jest.advanceTimersToNextTimer(); + }); - expect(spy).toHaveBeenCalledTimes(1); - }); + expect(spy).toHaveBeenCalledTimes(1); + }); - it('should cancel animation frame on unmount', () => { - const spy = jest.fn(); - const { rerender, unmount } = renderHook( - (dep) => { - useRafEffect(spy, [dep]); - }, - { - initialProps: 1, - } - ); + it('should cancel animation frame on unmount', () => { + const spy = jest.fn(); + const { rerender, unmount } = renderHook( + (dep) => { + useRafEffect(spy, [dep]); + }, + { + initialProps: 1, + } + ); - expect(spy).toHaveBeenCalledTimes(0); + expect(spy).toHaveBeenCalledTimes(0); - rerender(2); + rerender(2); - expect(spy).toHaveBeenCalledTimes(0); + expect(spy).toHaveBeenCalledTimes(0); - unmount(); + unmount(); - act(() => { - jest.advanceTimersToNextTimer(); - }); + act(() => { + jest.advanceTimersToNextTimer(); + }); - expect(spy).toHaveBeenCalledTimes(0); - }); + expect(spy).toHaveBeenCalledTimes(0); + }); }); diff --git a/src/useRafEffect/__tests__/ssr.ts b/src/useRafEffect/__tests__/ssr.ts index 7dbc33c00..7861cf465 100644 --- a/src/useRafEffect/__tests__/ssr.ts +++ b/src/useRafEffect/__tests__/ssr.ts @@ -2,14 +2,14 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useRafEffect } from '../..'; describe('useRafEffect', () => { - it('should be defined', () => { - expect(useRafEffect).toBeDefined(); - }); + it('should be defined', () => { + expect(useRafEffect).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useRafEffect(() => {}, []); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useRafEffect(() => {}, []); + }); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useRafEffect/index.ts b/src/useRafEffect/index.ts index 76d110458..fd3e4ebae 100644 --- a/src/useRafEffect/index.ts +++ b/src/useRafEffect/index.ts @@ -9,15 +9,15 @@ import { useRafCallback } from '../useRafCallback'; * @param deps Dependencies list that will be passed to underlying `useEffect`. */ export function useRafEffect(callback: (...args: any[]) => void, deps: DependencyList): void { - const [rafCallback, cancelRaf] = useRafCallback(callback); + const [rafCallback, cancelRaf] = useRafCallback(callback); - useEffect( - () => { - rafCallback(); + useEffect( + () => { + rafCallback(); - return cancelRaf; - }, + return cancelRaf; + }, - deps - ); + deps + ); } diff --git a/src/useRafState/__docs__/example.stories.tsx b/src/useRafState/__docs__/example.stories.tsx index 31c806f81..1e562339b 100644 --- a/src/useRafState/__docs__/example.stories.tsx +++ b/src/useRafState/__docs__/example.stories.tsx @@ -2,30 +2,30 @@ import * as React from 'react'; import { useMountEffect, useRafState } from '../..'; export function Example() { - const [state, setState] = useRafState({ x: 0, y: 0 }); + const [state, setState] = useRafState({ x: 0, y: 0 }); - useMountEffect(() => { - const onMouseMove = (event: MouseEvent) => { - setState({ x: event.clientX, y: event.clientY }); - }; + useMountEffect(() => { + const onMouseMove = (event: MouseEvent) => { + setState({ x: event.clientX, y: event.clientY }); + }; - const onTouchMove = (event: TouchEvent) => { - setState({ x: event.changedTouches[0].clientX, y: event.changedTouches[0].clientY }); - }; + const onTouchMove = (event: TouchEvent) => { + setState({ x: event.changedTouches[0].clientX, y: event.changedTouches[0].clientY }); + }; - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('touchmove', onTouchMove); + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('touchmove', onTouchMove); - return () => { - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('touchmove', onTouchMove); - }; - }); + return () => { + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('touchmove', onTouchMove); + }; + }); - return ( -
- Below state will be updated on mouse/cursor move within animation frame -
{JSON.stringify(state, null, 2)}
-
- ); + return ( +
+ Below state will be updated on mouse/cursor move within animation frame +
{JSON.stringify(state, null, 2)}
+
+ ); } diff --git a/src/useRafState/__docs__/story.mdx b/src/useRafState/__docs__/story.mdx index 40770bd03..585604daf 100644 --- a/src/useRafState/__docs__/story.mdx +++ b/src/useRafState/__docs__/story.mdx @@ -14,7 +14,7 @@ Like `React.useState`, but state is only updated within animation frame. #### Example - + ## Reference @@ -22,8 +22,8 @@ Like `React.useState`, but state is only updated within animation frame. ```ts export function useRafState(initialState: S | (() => S)): [S, Dispatch>]; export function useRafState(): [ - S | undefined, - Dispatch> + S | undefined, + Dispatch> ]; ``` diff --git a/src/useRafState/__tests__/dom.ts b/src/useRafState/__tests__/dom.ts index 0a54a099d..f7b674461 100644 --- a/src/useRafState/__tests__/dom.ts +++ b/src/useRafState/__tests__/dom.ts @@ -2,68 +2,68 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { useRafState } from '../..'; describe('useRafState', () => { - const raf = global.requestAnimationFrame; - const caf = global.cancelAnimationFrame; + const raf = global.requestAnimationFrame; + const caf = global.cancelAnimationFrame; - beforeAll(() => { - jest.useFakeTimers(); + beforeAll(() => { + jest.useFakeTimers(); - global.requestAnimationFrame = (cb) => setTimeout(cb); - global.cancelAnimationFrame = (cb) => { - clearTimeout(cb); - }; - }); + global.requestAnimationFrame = (cb) => setTimeout(cb); + global.cancelAnimationFrame = (cb) => { + clearTimeout(cb); + }; + }); - afterEach(() => { - jest.clearAllTimers(); - }); + afterEach(() => { + jest.clearAllTimers(); + }); - afterAll(() => { - jest.useRealTimers(); + afterAll(() => { + jest.useRealTimers(); - global.requestAnimationFrame = raf; - global.cancelAnimationFrame = caf; - }); + global.requestAnimationFrame = raf; + global.cancelAnimationFrame = caf; + }); - it('should be defined', () => { - expect(useRafState).toBeDefined(); - }); + it('should be defined', () => { + expect(useRafState).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useRafState()); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useRafState()); + expect(result.error).toBeUndefined(); + }); - it('should not update state unless animation frame', () => { - const { result } = renderHook(() => useRafState()); + it('should not update state unless animation frame', () => { + const { result } = renderHook(() => useRafState()); - act(() => { - result.current[1](1); - result.current[1](2); - result.current[1](3); - }); + act(() => { + result.current[1](1); + result.current[1](2); + result.current[1](3); + }); - expect(result.current[0]).toBeUndefined(); + expect(result.current[0]).toBeUndefined(); - act(() => { - jest.advanceTimersToNextTimer(); - }); + act(() => { + jest.advanceTimersToNextTimer(); + }); - expect(result.current[0]).toBe(3); - expect(result.all.length).toBe(2); - }); + expect(result.current[0]).toBe(3); + expect(result.all.length).toBe(2); + }); - it('should cancel animation frame on unmount', () => { - const { result, unmount } = renderHook(() => useRafState()); + it('should cancel animation frame on unmount', () => { + const { result, unmount } = renderHook(() => useRafState()); - act(() => { - result.current[1](1); - result.current[1](2); - result.current[1](3); - }); + act(() => { + result.current[1](1); + result.current[1](2); + result.current[1](3); + }); - unmount(); + unmount(); - expect(result.current[0]).toBeUndefined(); - }); + expect(result.current[0]).toBeUndefined(); + }); }); diff --git a/src/useRafState/__tests__/ssr.ts b/src/useRafState/__tests__/ssr.ts index 06591bba5..5f627ce5f 100644 --- a/src/useRafState/__tests__/ssr.ts +++ b/src/useRafState/__tests__/ssr.ts @@ -2,12 +2,12 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useRafState } from '../..'; describe('useRafState', () => { - it('should be defined', () => { - expect(useRafState).toBeDefined(); - }); + it('should be defined', () => { + expect(useRafState).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useRafState()); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useRafState()); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useRafState/index.ts b/src/useRafState/index.ts index b9e160e16..92ee668be 100644 --- a/src/useRafState/index.ts +++ b/src/useRafState/index.ts @@ -4,21 +4,21 @@ import { useUnmountEffect } from '../useUnmountEffect'; export function useRafState(initialState: S | (() => S)): [S, Dispatch>]; export function useRafState(): [ - S | undefined, - Dispatch> + S | undefined, + Dispatch> ]; /** * Like `React.useState`, but state is only updated within animation frame. */ export function useRafState( - initialState?: S | (() => S) + initialState?: S | (() => S) ): [S | undefined, Dispatch>] { - const [state, innerSetState] = useState(initialState); + const [state, innerSetState] = useState(initialState); - const [setState, cancelRaf] = useRafCallback(innerSetState); + const [setState, cancelRaf] = useRafCallback(innerSetState); - useUnmountEffect(cancelRaf); + useUnmountEffect(cancelRaf); - return [state, setState as Dispatch>]; + return [state, setState as Dispatch>]; } diff --git a/src/useRenderCount/__docs__/example.stories.tsx b/src/useRenderCount/__docs__/example.stories.tsx index b02df7d04..6dd2325cb 100644 --- a/src/useRenderCount/__docs__/example.stories.tsx +++ b/src/useRenderCount/__docs__/example.stories.tsx @@ -2,16 +2,16 @@ import React from 'react'; import { useRerender, useRenderCount } from '../..'; export function Example() { - const renders = useRenderCount(); - const rerender = useRerender(); + const renders = useRenderCount(); + const rerender = useRerender(); - return ( -
-
This component has rendered {renders} time(s)
-
- -
- ); + return ( +
+
This component has rendered {renders} time(s)
+
+ +
+ ); } diff --git a/src/useRenderCount/__docs__/story.mdx b/src/useRenderCount/__docs__/story.mdx index 24abef5b1..7ce4e615f 100644 --- a/src/useRenderCount/__docs__/story.mdx +++ b/src/useRenderCount/__docs__/story.mdx @@ -11,7 +11,7 @@ Tracks component's render count including the first render. #### Example - + ## Reference diff --git a/src/useRenderCount/__tests__/dom.ts b/src/useRenderCount/__tests__/dom.ts index 0863c3412..fd22e6b74 100644 --- a/src/useRenderCount/__tests__/dom.ts +++ b/src/useRenderCount/__tests__/dom.ts @@ -2,15 +2,15 @@ import { renderHook } from '@testing-library/react-hooks'; import { useRenderCount } from '../..'; describe('useRendersCount', () => { - it('should be defined', () => { - expect(useRenderCount).toBeDefined(); - }); + it('should be defined', () => { + expect(useRenderCount).toBeDefined(); + }); - it('should return amount of renders performed', () => { - const { result, rerender } = renderHook(useRenderCount); + it('should return amount of renders performed', () => { + const { result, rerender } = renderHook(useRenderCount); - expect(result.current).toBe(1); - rerender(); - expect(result.current).toBe(2); - }); + expect(result.current).toBe(1); + rerender(); + expect(result.current).toBe(2); + }); }); diff --git a/src/useRenderCount/__tests__/ssr.ts b/src/useRenderCount/__tests__/ssr.ts index fc3843a15..181c1b9af 100644 --- a/src/useRenderCount/__tests__/ssr.ts +++ b/src/useRenderCount/__tests__/ssr.ts @@ -2,13 +2,13 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useRenderCount } from '../..'; describe('useRendersCount', () => { - it('should be defined', () => { - expect(useRenderCount).toBeDefined(); - }); + it('should be defined', () => { + expect(useRenderCount).toBeDefined(); + }); - it('should return proper amount of renders performed', () => { - const { result } = renderHook(useRenderCount); + it('should return proper amount of renders performed', () => { + const { result } = renderHook(useRenderCount); - expect(result.current).toBe(1); - }); + expect(result.current).toBe(1); + }); }); diff --git a/src/useRenderCount/index.ts b/src/useRenderCount/index.ts index cbd3d7eee..bb4f7b202 100644 --- a/src/useRenderCount/index.ts +++ b/src/useRenderCount/index.ts @@ -4,7 +4,7 @@ import { useRef } from 'react'; * Tracks component's render count including first render. */ export function useRenderCount(): number { - const rendersCount = useRef(0); + const rendersCount = useRef(0); - return ++rendersCount.current; + return ++rendersCount.current; } diff --git a/src/useRerender/__docs__/example.stories.tsx b/src/useRerender/__docs__/example.stories.tsx index d88b3bb21..6ad8a6144 100644 --- a/src/useRerender/__docs__/example.stories.tsx +++ b/src/useRerender/__docs__/example.stories.tsx @@ -2,16 +2,16 @@ import * as React from 'react'; import { useRenderCount, useRerender } from '../..'; export function Example() { - const renders = useRenderCount(); - const rerender = useRerender(); + const renders = useRenderCount(); + const rerender = useRerender(); - return ( -
-
This component has rendered {renders} time(s)
-
- -
- ); + return ( +
+
This component has rendered {renders} time(s)
+
+ +
+ ); } diff --git a/src/useRerender/__docs__/story.mdx b/src/useRerender/__docs__/story.mdx index e54a80883..afa922c16 100644 --- a/src/useRerender/__docs__/story.mdx +++ b/src/useRerender/__docs__/story.mdx @@ -13,7 +13,7 @@ Return callback function that re-renders component. #### Example - + ## Reference diff --git a/src/useRerender/__tests__/dom.ts b/src/useRerender/__tests__/dom.ts index 82b24e876..14b3fcef9 100644 --- a/src/useRerender/__tests__/dom.ts +++ b/src/useRerender/__tests__/dom.ts @@ -3,45 +3,45 @@ import { useRef } from 'react'; import { useRerender } from '../..'; describe('useRerender', () => { - it('should be defined', () => { - expect(useRerender).toBeDefined(); - }); - - it('should return same function on each re-render', () => { - const { result, rerender } = renderHook(() => useRerender()); - const fn1 = result.current; - rerender(); - const fn2 = result.current; - rerender(); - const fn3 = result.current; - - expect(fn1).toBeInstanceOf(Function); - expect(fn1).toBe(fn2); - expect(fn2).toBe(fn3); - }); - - it('should rerender component on returned function invocation', () => { - const { result } = renderHook(() => { - const cnt = useRef(0); - const rerender = useRerender(); - - return [rerender, ++cnt.current] as const; - }); - - expect(result.current[1]).toBe(1); - - act(() => { - // https://github.com/react-hookz/web/issues/691 - result.current[0](); - result.current[0](); - }); - expect(result.current[1]).toBe(2); - - act(() => { - result.current[0](); - result.current[0](); - result.current[0](); - }); - expect(result.current[1]).toBe(3); - }); + it('should be defined', () => { + expect(useRerender).toBeDefined(); + }); + + it('should return same function on each re-render', () => { + const { result, rerender } = renderHook(() => useRerender()); + const fn1 = result.current; + rerender(); + const fn2 = result.current; + rerender(); + const fn3 = result.current; + + expect(fn1).toBeInstanceOf(Function); + expect(fn1).toBe(fn2); + expect(fn2).toBe(fn3); + }); + + it('should rerender component on returned function invocation', () => { + const { result } = renderHook(() => { + const cnt = useRef(0); + const rerender = useRerender(); + + return [rerender, ++cnt.current] as const; + }); + + expect(result.current[1]).toBe(1); + + act(() => { + // https://github.com/react-hookz/web/issues/691 + result.current[0](); + result.current[0](); + }); + expect(result.current[1]).toBe(2); + + act(() => { + result.current[0](); + result.current[0](); + result.current[0](); + }); + expect(result.current[1]).toBe(3); + }); }); diff --git a/src/useRerender/__tests__/ssr.ts b/src/useRerender/__tests__/ssr.ts index 53cbbc7dc..525abf855 100644 --- a/src/useRerender/__tests__/ssr.ts +++ b/src/useRerender/__tests__/ssr.ts @@ -3,26 +3,26 @@ import { useRef } from 'react'; import { useRerender } from '../..'; describe('useRerender', () => { - it('should be defined', () => { - expect(useRerender).toBeDefined(); - }); + it('should be defined', () => { + expect(useRerender).toBeDefined(); + }); - it('should do nothing on returned function invocation', () => { - const { result } = renderHook(() => { - const cnt = useRef(0); - const rerender = useRerender(); + it('should do nothing on returned function invocation', () => { + const { result } = renderHook(() => { + const cnt = useRef(0); + const rerender = useRerender(); - return [rerender, ++cnt.current] as const; - }); + return [rerender, ++cnt.current] as const; + }); - expect(result.current[1]).toBe(1); - act(() => { - result.current[0](); - }); - expect(result.current[1]).toBe(1); - act(() => { - result.current[0](); - }); - expect(result.current[1]).toBe(1); - }); + expect(result.current[1]).toBe(1); + act(() => { + result.current[0](); + }); + expect(result.current[1]).toBe(1); + act(() => { + result.current[0](); + }); + expect(result.current[1]).toBe(1); + }); }); diff --git a/src/useRerender/index.ts b/src/useRerender/index.ts index 4894b73d3..da7f8846b 100644 --- a/src/useRerender/index.ts +++ b/src/useRerender/index.ts @@ -6,9 +6,9 @@ const stateChanger = (state: number) => (state + 1) % Number.MAX_SAFE_INTEGER; * Return callback function that re-renders component. */ export function useRerender(): () => void { - const [, setState] = useState(0); + const [, setState] = useState(0); - return useCallback(() => { - setState(stateChanger); - }, []); + return useCallback(() => { + setState(stateChanger); + }, []); } diff --git a/src/useResizeObserver/__docs__/example.stories.tsx b/src/useResizeObserver/__docs__/example.stories.tsx index 0fe4d45bd..d96399e18 100644 --- a/src/useResizeObserver/__docs__/example.stories.tsx +++ b/src/useResizeObserver/__docs__/example.stories.tsx @@ -3,56 +3,56 @@ import { useRef, useState } from 'react'; import { type UseResizeObserverCallback, useDebouncedCallback, useResizeObserver } from '../..'; export function Example() { - const ref = useRef(null); - const [rect, setRect] = useState(); - useResizeObserver(ref, (e) => { - setRect(e.contentRect); - }); + const ref = useRef(null); + const [rect, setRect] = useState(); + useResizeObserver(ref, (e) => { + setRect(e.contentRect); + }); - return ( -
-
{JSON.stringify(rect)}
-
- resize me UwU -
-
- ); + return ( +
+
{JSON.stringify(rect)}
+
+ resize me UwU +
+
+ ); } export function ExampleDebounced() { - const ref = useRef(null); - const [rect, setRect] = useState(); - const cb = useDebouncedCallback( - ((e) => { - setRect(e.contentRect); - }) as UseResizeObserverCallback, - [setRect], - 500 - ); - useResizeObserver(ref, cb); + const ref = useRef(null); + const [rect, setRect] = useState(); + const cb = useDebouncedCallback( + ((e) => { + setRect(e.contentRect); + }) as UseResizeObserverCallback, + [setRect], + 500 + ); + useResizeObserver(ref, cb); - return ( -
-
{JSON.stringify(rect)}
-
- resize me UwU -
-
- ); + return ( +
+
{JSON.stringify(rect)}
+
+ resize me UwU +
+
+ ); } diff --git a/src/useResizeObserver/__docs__/story.mdx b/src/useResizeObserver/__docs__/story.mdx index d60e6ecbd..08e753cbd 100644 --- a/src/useResizeObserver/__docs__/story.mdx +++ b/src/useResizeObserver/__docs__/story.mdx @@ -20,22 +20,22 @@ Invokes a callback whenever ResizeObserver detects a change to target's size. #### Example - Below component uses direct invocation so it is not so optimal in terms of CPU usage, but it gains - most recent data. - - As `useResizeObserver` does not apply any debounce or throttle mechanisms to received callback - - it is up to developer to do so if needed. Below example is almost same as previous but state is - updated within 500ms debounce. - + Below component uses direct invocation so it is not so optimal in terms of CPU usage, but it gains + most recent data. + + As `useResizeObserver` does not apply any debounce or throttle mechanisms to received callback - + it is up to developer to do so if needed. Below example is almost same as previous but state is + updated within 500ms debounce. + ## Reference ```ts export function useResizeObserver( - target: React.RefObject | T | null, - callback: (entry: ResizeObserverEntry) => void, - enabled = true + target: React.RefObject | T | null, + callback: (entry: ResizeObserverEntry) => void, + enabled = true ): void; ``` diff --git a/src/useResizeObserver/__tests__/dom.ts b/src/useResizeObserver/__tests__/dom.ts index f29f06293..1f34e76dc 100644 --- a/src/useResizeObserver/__tests__/dom.ts +++ b/src/useResizeObserver/__tests__/dom.ts @@ -3,228 +3,228 @@ import { useResizeObserver } from '../..'; import Mock = jest.Mock; describe('useResizeObserver', () => { - const observeSpy = jest.fn(); - const unobserveSpy = jest.fn(); - const disconnectSpy = jest.fn(); - - let ResizeObserverSpy: Mock; - const initialRO = global.ResizeObserver; - - beforeAll(() => { - ResizeObserverSpy = jest.fn(() => ({ - observe: observeSpy, - unobserve: unobserveSpy, - disconnect: disconnectSpy, - })); - - global.ResizeObserver = ResizeObserverSpy; - jest.useFakeTimers(); - }); - - beforeEach(() => { - observeSpy.mockClear(); - unobserveSpy.mockClear(); - disconnectSpy.mockClear(); - }); - - afterAll(() => { - global.ResizeObserver = initialRO; - jest.useRealTimers(); - }); - - it('should be defined', () => { - expect(useResizeObserver).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => { - useResizeObserver(null, () => {}); - }); - - expect(result.error).toBeUndefined(); - }); - - it('should create ResizeObserver instance only on first hook render', () => { - expect(ResizeObserverSpy).toHaveBeenCalledTimes(1); - - renderHook(() => { - useResizeObserver(null, () => {}); - }); - renderHook(() => { - useResizeObserver(null, () => {}); - }); - - expect(ResizeObserverSpy).toHaveBeenCalledTimes(1); - }); - - it('should subscribe in case ref first was empty but then gained element', () => { - const div = document.createElement('div'); - const ref: React.MutableRefObject = { current: null }; - const spy = jest.fn(); - - const { rerender } = renderHook( - ({ ref }) => { - useResizeObserver(ref, spy); - }, - { - initialProps: { ref }, - } - ); - - expect(observeSpy).toHaveBeenCalledTimes(0); - - ref.current = div; - rerender({ ref }); - - expect(observeSpy).toHaveBeenCalledTimes(1); - - const entry = { - target: div, - contentRect: {}, - borderBoxSize: {}, - contentBoxSize: {}, - } as unknown as ResizeObserverEntry; - - ResizeObserverSpy.mock.calls[0][0]([entry]); - - expect(spy).not.toHaveBeenCalledWith(entry); - - jest.advanceTimersByTime(1); - - expect(spy).toHaveBeenCalledWith(entry); - }); - - it('should invoke each callback listening same element asynchronously using setTimeout0', () => { - const div = document.createElement('div'); - const spy1 = jest.fn(); - const spy2 = jest.fn(); - - renderHook(() => { - useResizeObserver(div, spy1); - }); - renderHook(() => { - useResizeObserver(div, spy2); - }); - - expect(observeSpy).toHaveBeenCalledTimes(1); - - const entry = { - target: div, - contentRect: {}, - borderBoxSize: {}, - contentBoxSize: {}, - } as unknown as ResizeObserverEntry; - - ResizeObserverSpy.mock.calls[0][0]([entry]); - - expect(spy1).not.toHaveBeenCalledWith(entry); - expect(spy2).not.toHaveBeenCalledWith(entry); - - jest.advanceTimersByTime(1); - - expect(spy1).toHaveBeenCalledWith(entry); - expect(spy2).toHaveBeenCalledWith(entry); - }); - - it('should invoke each callback listening different element', () => { - const div = document.createElement('div'); - const div2 = document.createElement('div'); - const spy1 = jest.fn(); - const spy2 = jest.fn(); - - renderHook(() => { - useResizeObserver(div, spy1); - }); - renderHook(() => { - useResizeObserver({ current: div2 }, spy2); - }); - - expect(observeSpy).toHaveBeenCalledTimes(2); - - const entry1 = { - target: div, - contentRect: {}, - borderBoxSize: {}, - contentBoxSize: {}, - } as unknown as ResizeObserverEntry; - const entry2 = { - target: div2, - contentRect: {}, - borderBoxSize: {}, - contentBoxSize: {}, - } as unknown as ResizeObserverEntry; - - ResizeObserverSpy.mock.calls[0][0]([entry1, entry2]); - - expect(spy1).not.toHaveBeenCalledWith(entry1); - expect(spy2).not.toHaveBeenCalledWith(entry2); - - jest.advanceTimersByTime(1); - - expect(spy1).toHaveBeenCalledWith(entry1); - expect(spy2).toHaveBeenCalledWith(entry2); - }); - - it('should unsubscribe on component unmount', () => { - const div = document.createElement('div'); - const spy = jest.fn(); - const { unmount } = renderHook(() => { - useResizeObserver(div, spy); - }); - - expect(observeSpy).toHaveBeenCalledTimes(1); - expect(observeSpy).toHaveBeenCalledWith(div); - expect(unobserveSpy).toHaveBeenCalledTimes(0); - - unmount(); - - expect(observeSpy).toHaveBeenCalledTimes(1); - expect(unobserveSpy).toHaveBeenCalledTimes(1); - expect(unobserveSpy).toHaveBeenCalledWith(div); - }); - - describe('disabled observer', () => { - it('should not subscribe in case observer is disabled', () => { - const div = document.createElement('div'); - const div2 = document.createElement('div'); - const spy1 = jest.fn(); - const spy2 = jest.fn(); - - renderHook(() => { - useResizeObserver(div, spy1); - }); - renderHook(() => { - useResizeObserver({ current: div2 }, spy2, false); - }); - - expect(observeSpy).toHaveBeenCalledTimes(1); - }); - - it('should unsubscribe and resubscribe in case of observer toggling', () => { - const div = document.createElement('div'); - const spy1 = jest.fn(); - - const { rerender } = renderHook( - ({ enabled }) => { - useResizeObserver(div, spy1, enabled); - }, - { - initialProps: { enabled: false }, - } - ); - - expect(observeSpy).toHaveBeenCalledTimes(0); - expect(unobserveSpy).toHaveBeenCalledTimes(0); - - rerender({ enabled: true }); - - expect(observeSpy).toHaveBeenCalledTimes(1); - expect(unobserveSpy).toHaveBeenCalledTimes(0); - - rerender({ enabled: false }); - - expect(observeSpy).toHaveBeenCalledTimes(1); - expect(unobserveSpy).toHaveBeenCalledTimes(1); - }); - }); + const observeSpy = jest.fn(); + const unobserveSpy = jest.fn(); + const disconnectSpy = jest.fn(); + + let ResizeObserverSpy: Mock; + const initialRO = global.ResizeObserver; + + beforeAll(() => { + ResizeObserverSpy = jest.fn(() => ({ + observe: observeSpy, + unobserve: unobserveSpy, + disconnect: disconnectSpy, + })); + + global.ResizeObserver = ResizeObserverSpy; + jest.useFakeTimers(); + }); + + beforeEach(() => { + observeSpy.mockClear(); + unobserveSpy.mockClear(); + disconnectSpy.mockClear(); + }); + + afterAll(() => { + global.ResizeObserver = initialRO; + jest.useRealTimers(); + }); + + it('should be defined', () => { + expect(useResizeObserver).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => { + useResizeObserver(null, () => {}); + }); + + expect(result.error).toBeUndefined(); + }); + + it('should create ResizeObserver instance only on first hook render', () => { + expect(ResizeObserverSpy).toHaveBeenCalledTimes(1); + + renderHook(() => { + useResizeObserver(null, () => {}); + }); + renderHook(() => { + useResizeObserver(null, () => {}); + }); + + expect(ResizeObserverSpy).toHaveBeenCalledTimes(1); + }); + + it('should subscribe in case ref first was empty but then gained element', () => { + const div = document.createElement('div'); + const ref: React.MutableRefObject = { current: null }; + const spy = jest.fn(); + + const { rerender } = renderHook( + ({ ref }) => { + useResizeObserver(ref, spy); + }, + { + initialProps: { ref }, + } + ); + + expect(observeSpy).toHaveBeenCalledTimes(0); + + ref.current = div; + rerender({ ref }); + + expect(observeSpy).toHaveBeenCalledTimes(1); + + const entry = { + target: div, + contentRect: {}, + borderBoxSize: {}, + contentBoxSize: {}, + } as unknown as ResizeObserverEntry; + + ResizeObserverSpy.mock.calls[0][0]([entry]); + + expect(spy).not.toHaveBeenCalledWith(entry); + + jest.advanceTimersByTime(1); + + expect(spy).toHaveBeenCalledWith(entry); + }); + + it('should invoke each callback listening same element asynchronously using setTimeout0', () => { + const div = document.createElement('div'); + const spy1 = jest.fn(); + const spy2 = jest.fn(); + + renderHook(() => { + useResizeObserver(div, spy1); + }); + renderHook(() => { + useResizeObserver(div, spy2); + }); + + expect(observeSpy).toHaveBeenCalledTimes(1); + + const entry = { + target: div, + contentRect: {}, + borderBoxSize: {}, + contentBoxSize: {}, + } as unknown as ResizeObserverEntry; + + ResizeObserverSpy.mock.calls[0][0]([entry]); + + expect(spy1).not.toHaveBeenCalledWith(entry); + expect(spy2).not.toHaveBeenCalledWith(entry); + + jest.advanceTimersByTime(1); + + expect(spy1).toHaveBeenCalledWith(entry); + expect(spy2).toHaveBeenCalledWith(entry); + }); + + it('should invoke each callback listening different element', () => { + const div = document.createElement('div'); + const div2 = document.createElement('div'); + const spy1 = jest.fn(); + const spy2 = jest.fn(); + + renderHook(() => { + useResizeObserver(div, spy1); + }); + renderHook(() => { + useResizeObserver({ current: div2 }, spy2); + }); + + expect(observeSpy).toHaveBeenCalledTimes(2); + + const entry1 = { + target: div, + contentRect: {}, + borderBoxSize: {}, + contentBoxSize: {}, + } as unknown as ResizeObserverEntry; + const entry2 = { + target: div2, + contentRect: {}, + borderBoxSize: {}, + contentBoxSize: {}, + } as unknown as ResizeObserverEntry; + + ResizeObserverSpy.mock.calls[0][0]([entry1, entry2]); + + expect(spy1).not.toHaveBeenCalledWith(entry1); + expect(spy2).not.toHaveBeenCalledWith(entry2); + + jest.advanceTimersByTime(1); + + expect(spy1).toHaveBeenCalledWith(entry1); + expect(spy2).toHaveBeenCalledWith(entry2); + }); + + it('should unsubscribe on component unmount', () => { + const div = document.createElement('div'); + const spy = jest.fn(); + const { unmount } = renderHook(() => { + useResizeObserver(div, spy); + }); + + expect(observeSpy).toHaveBeenCalledTimes(1); + expect(observeSpy).toHaveBeenCalledWith(div); + expect(unobserveSpy).toHaveBeenCalledTimes(0); + + unmount(); + + expect(observeSpy).toHaveBeenCalledTimes(1); + expect(unobserveSpy).toHaveBeenCalledTimes(1); + expect(unobserveSpy).toHaveBeenCalledWith(div); + }); + + describe('disabled observer', () => { + it('should not subscribe in case observer is disabled', () => { + const div = document.createElement('div'); + const div2 = document.createElement('div'); + const spy1 = jest.fn(); + const spy2 = jest.fn(); + + renderHook(() => { + useResizeObserver(div, spy1); + }); + renderHook(() => { + useResizeObserver({ current: div2 }, spy2, false); + }); + + expect(observeSpy).toHaveBeenCalledTimes(1); + }); + + it('should unsubscribe and resubscribe in case of observer toggling', () => { + const div = document.createElement('div'); + const spy1 = jest.fn(); + + const { rerender } = renderHook( + ({ enabled }) => { + useResizeObserver(div, spy1, enabled); + }, + { + initialProps: { enabled: false }, + } + ); + + expect(observeSpy).toHaveBeenCalledTimes(0); + expect(unobserveSpy).toHaveBeenCalledTimes(0); + + rerender({ enabled: true }); + + expect(observeSpy).toHaveBeenCalledTimes(1); + expect(unobserveSpy).toHaveBeenCalledTimes(0); + + rerender({ enabled: false }); + + expect(observeSpy).toHaveBeenCalledTimes(1); + expect(unobserveSpy).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/src/useResizeObserver/__tests__/ssr.ts b/src/useResizeObserver/__tests__/ssr.ts index 897af695d..c34db6a62 100644 --- a/src/useResizeObserver/__tests__/ssr.ts +++ b/src/useResizeObserver/__tests__/ssr.ts @@ -2,15 +2,15 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useResizeObserver } from '../..'; describe('useResizeObserver', () => { - it('should be defined', () => { - expect(useResizeObserver).toBeDefined(); - }); + it('should be defined', () => { + expect(useResizeObserver).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useResizeObserver(null, () => {}); - }); + it('should render', () => { + const { result } = renderHook(() => { + useResizeObserver(null, () => {}); + }); - expect(result.error).toBeUndefined(); - }); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useResizeObserver/index.ts b/src/useResizeObserver/index.ts index 2354fc646..bc9d677eb 100644 --- a/src/useResizeObserver/index.ts +++ b/src/useResizeObserver/index.ts @@ -5,66 +5,66 @@ import { isBrowser } from '../util/const'; export type UseResizeObserverCallback = (entry: ResizeObserverEntry) => void; type ResizeObserverSingleton = { - observer: ResizeObserver; - subscribe: (target: Element, callback: UseResizeObserverCallback) => void; - unsubscribe: (target: Element, callback: UseResizeObserverCallback) => void; + observer: ResizeObserver; + subscribe: (target: Element, callback: UseResizeObserverCallback) => void; + unsubscribe: (target: Element, callback: UseResizeObserverCallback) => void; }; let observerSingleton: ResizeObserverSingleton; function getResizeObserver(): ResizeObserverSingleton | undefined { - if (!isBrowser) return undefined; - - if (observerSingleton) return observerSingleton; - - const callbacks = new Map>(); - - const observer = new ResizeObserver((entries) => { - entries.forEach((entry) => - callbacks.get(entry.target)?.forEach((cb) => - setTimeout(() => { - cb(entry); - }, 0) - ) - ); - }); - - observerSingleton = { - observer, - subscribe(target, callback) { - let cbs = callbacks.get(target); - - if (!cbs) { - // If target has no observers yet - register it - cbs = new Set(); - callbacks.set(target, cbs); - observer.observe(target); - } - - // As Set is duplicate-safe - simply add callback on each call - cbs.add(callback); - }, - unsubscribe(target, callback) { - const cbs = callbacks.get(target); - - // Else branch should never occur in case of normal execution - // because callbacks map is hidden in closure - it is impossible to - // simulate situation with non-existent `cbs` Set - /* istanbul ignore else */ - if (cbs) { - // Remove current observer - cbs.delete(callback); - - if (!cbs.size) { - // If no observers left unregister target completely - callbacks.delete(target); - observer.unobserve(target); - } - } - }, - }; - - return observerSingleton; + if (!isBrowser) return undefined; + + if (observerSingleton) return observerSingleton; + + const callbacks = new Map>(); + + const observer = new ResizeObserver((entries) => { + entries.forEach((entry) => + callbacks.get(entry.target)?.forEach((cb) => + setTimeout(() => { + cb(entry); + }, 0) + ) + ); + }); + + observerSingleton = { + observer, + subscribe(target, callback) { + let cbs = callbacks.get(target); + + if (!cbs) { + // If target has no observers yet - register it + cbs = new Set(); + callbacks.set(target, cbs); + observer.observe(target); + } + + // As Set is duplicate-safe - simply add callback on each call + cbs.add(callback); + }, + unsubscribe(target, callback) { + const cbs = callbacks.get(target); + + // Else branch should never occur in case of normal execution + // because callbacks map is hidden in closure - it is impossible to + // simulate situation with non-existent `cbs` Set + /* istanbul ignore else */ + if (cbs) { + // Remove current observer + cbs.delete(callback); + + if (!cbs.size) { + // If no observers left unregister target completely + callbacks.delete(target); + observer.unobserve(target); + } + } + }, + }; + + return observerSingleton; } /** @@ -75,43 +75,43 @@ function getResizeObserver(): ResizeObserverSingleton | undefined { * @param enabled Whether resize observer is enabled or not. */ export function useResizeObserver( - target: RefObject | T | null, - callback: UseResizeObserverCallback, - enabled = true + target: RefObject | T | null, + callback: UseResizeObserverCallback, + enabled = true ): void { - const ro = enabled && getResizeObserver(); - const cb = useSyncedRef(callback); + const ro = enabled && getResizeObserver(); + const cb = useSyncedRef(callback); - const tgt = target && 'current' in target ? target.current : target; + const tgt = target && 'current' in target ? target.current : target; - useEffect(() => { - // This secondary target resolve required for case when we receive ref object, which, most - // likely, contains null during render stage, but already populated with element during - // effect stage. + useEffect(() => { + // This secondary target resolve required for case when we receive ref object, which, most + // likely, contains null during render stage, but already populated with element during + // effect stage. - const tgt = target && 'current' in target ? target.current : target; + const tgt = target && 'current' in target ? target.current : target; - if (!ro || !tgt) return; + if (!ro || !tgt) return; - // As unsubscription in internals of our ResizeObserver abstraction can - // happen a bit later than effect cleanup invocation - we need a marker, - // that this handler should not be invoked anymore - let subscribed = true; + // As unsubscription in internals of our ResizeObserver abstraction can + // happen a bit later than effect cleanup invocation - we need a marker, + // that this handler should not be invoked anymore + let subscribed = true; - const handler: UseResizeObserverCallback = (...args) => { - // It is reinsurance for the highly asynchronous invocations, almost - // impossible to achieve in tests, thus excluding from LOC - /* istanbul ignore else */ - if (subscribed) { - cb.current(...args); - } - }; + const handler: UseResizeObserverCallback = (...args) => { + // It is reinsurance for the highly asynchronous invocations, almost + // impossible to achieve in tests, thus excluding from LOC + /* istanbul ignore else */ + if (subscribed) { + cb.current(...args); + } + }; - ro.subscribe(tgt, handler); + ro.subscribe(tgt, handler); - return () => { - subscribed = false; - ro.unsubscribe(tgt, handler); - }; - }, [tgt, ro]); + return () => { + subscribed = false; + ro.unsubscribe(tgt, handler); + }; + }, [tgt, ro]); } diff --git a/src/useScreenOrientation/__docs__/example.stories.tsx b/src/useScreenOrientation/__docs__/example.stories.tsx index 263c93519..7df1a4f57 100644 --- a/src/useScreenOrientation/__docs__/example.stories.tsx +++ b/src/useScreenOrientation/__docs__/example.stories.tsx @@ -2,16 +2,16 @@ import * as React from 'react'; import { useScreenOrientation } from '../..'; export function Example() { - const orientation = useScreenOrientation(); + const orientation = useScreenOrientation(); - return ( -
-
- Orientation: {orientation} -
-
- Render time: {new Date().toLocaleString()} -
-
- ); + return ( +
+
+ Orientation: {orientation} +
+
+ Render time: {new Date().toLocaleString()} +
+
+ ); } diff --git a/src/useScreenOrientation/__docs__/story.mdx b/src/useScreenOrientation/__docs__/story.mdx index edcf174bc..56d97583f 100644 --- a/src/useScreenOrientation/__docs__/story.mdx +++ b/src/useScreenOrientation/__docs__/story.mdx @@ -21,7 +21,7 @@ check screen orientation. #### Example - + ## Reference diff --git a/src/useScreenOrientation/__tests__/dom.ts b/src/useScreenOrientation/__tests__/dom.ts index ec3dd85b4..20838ee2c 100644 --- a/src/useScreenOrientation/__tests__/dom.ts +++ b/src/useScreenOrientation/__tests__/dom.ts @@ -2,76 +2,76 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { useScreenOrientation } from '../..'; describe('useScreenOrientation', () => { - // Have to copy implementation as jsdom lacks of it - type MutableMediaQueryList = { - matches: boolean; - media: string; - onchange: null; - addListener: jest.Mock; // Deprecated - removeListener: jest.Mock; // Deprecated - addEventListener: jest.Mock; - removeEventListener: jest.Mock; - dispatchEvent: jest.Mock; - }; + // Have to copy implementation as jsdom lacks of it + type MutableMediaQueryList = { + matches: boolean; + media: string; + onchange: null; + addListener: jest.Mock; // Deprecated + removeListener: jest.Mock; // Deprecated + addEventListener: jest.Mock; + removeEventListener: jest.Mock; + dispatchEvent: jest.Mock; + }; - const matchMediaMock = jest.fn(); - let initialMatchMedia: typeof window.matchMedia; + const matchMediaMock = jest.fn(); + let initialMatchMedia: typeof window.matchMedia; - beforeAll(() => { - initialMatchMedia = window.matchMedia; - Object.defineProperty(window, 'matchMedia', { - writable: true, - value: matchMediaMock, - }); - }); + beforeAll(() => { + initialMatchMedia = window.matchMedia; + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: matchMediaMock, + }); + }); - afterAll(() => { - window.matchMedia = initialMatchMedia; - }); + afterAll(() => { + window.matchMedia = initialMatchMedia; + }); - beforeEach(() => { - matchMediaMock.mockImplementation((query) => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), // Deprecated - removeListener: jest.fn(), // Deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })); - }); + beforeEach(() => { + matchMediaMock.mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })); + }); - afterEach(() => { - matchMediaMock.mockClear(); - }); + afterEach(() => { + matchMediaMock.mockClear(); + }); - it('should be defined', () => { - expect(useScreenOrientation).toBeDefined(); - }); + it('should be defined', () => { + expect(useScreenOrientation).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useScreenOrientation()); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useScreenOrientation()); + expect(result.error).toBeUndefined(); + }); - it('should initialize without value if initializeWithValue option is set to false', () => { - const { result } = renderHook(() => useScreenOrientation({ initializeWithValue: false })); - expect(result.all[0]).toBeUndefined(); - expect(result.all[1]).toBe('landscape'); - }); + it('should initialize without value if initializeWithValue option is set to false', () => { + const { result } = renderHook(() => useScreenOrientation({ initializeWithValue: false })); + expect(result.all[0]).toBeUndefined(); + expect(result.all[1]).toBe('landscape'); + }); - it('should return `portrait` in case media query matches and `landscape` otherwise', () => { - const { result } = renderHook(() => useScreenOrientation()); - expect(result.current).toBe('landscape'); + it('should return `portrait` in case media query matches and `landscape` otherwise', () => { + const { result } = renderHook(() => useScreenOrientation()); + expect(result.current).toBe('landscape'); - const mql = matchMediaMock.mock.results[0].value as MutableMediaQueryList; - mql.matches = true; + const mql = matchMediaMock.mock.results[0].value as MutableMediaQueryList; + mql.matches = true; - act(() => { - mql.addEventListener.mock.calls[0][1](); - }); + act(() => { + mql.addEventListener.mock.calls[0][1](); + }); - expect(result.current).toBe('portrait'); - }); + expect(result.current).toBe('portrait'); + }); }); diff --git a/src/useScreenOrientation/__tests__/ssr.ts b/src/useScreenOrientation/__tests__/ssr.ts index 56950b875..4b4a91069 100644 --- a/src/useScreenOrientation/__tests__/ssr.ts +++ b/src/useScreenOrientation/__tests__/ssr.ts @@ -2,12 +2,12 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useScreenOrientation } from '../..'; describe('useScreenOrientation', () => { - it('should be defined', () => { - expect(useScreenOrientation).toBeDefined(); - }); + it('should be defined', () => { + expect(useScreenOrientation).toBeDefined(); + }); - it('should render if initializeWithValue option is set to false', () => { - const { result } = renderHook(() => useScreenOrientation({ initializeWithValue: false })); - expect(result.error).toBeUndefined(); - }); + it('should render if initializeWithValue option is set to false', () => { + const { result } = renderHook(() => useScreenOrientation({ initializeWithValue: false })); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useScreenOrientation/index.ts b/src/useScreenOrientation/index.ts index 62115a8e0..692dbdb5e 100644 --- a/src/useScreenOrientation/index.ts +++ b/src/useScreenOrientation/index.ts @@ -3,7 +3,7 @@ import { useMediaQuery } from '../useMediaQuery'; export type ScreenOrientation = 'portrait' | 'landscape'; type UseScreenOrientationOptions = { - initializeWithValue?: boolean; + initializeWithValue?: boolean; }; /** @@ -13,11 +13,11 @@ type UseScreenOrientationOptions = { * hook uses CSS3 `orientation` media-query to check screen orientation. */ export function useScreenOrientation( - options?: UseScreenOrientationOptions + options?: UseScreenOrientationOptions ): ScreenOrientation | undefined { - const matches = useMediaQuery('(orientation: portrait)', { - initializeWithValue: options?.initializeWithValue ?? true, - }); + const matches = useMediaQuery('(orientation: portrait)', { + initializeWithValue: options?.initializeWithValue ?? true, + }); - return matches === undefined ? undefined : matches ? 'portrait' : 'landscape'; + return matches === undefined ? undefined : matches ? 'portrait' : 'landscape'; } diff --git a/src/useSessionStorageValue/__docs__/example.stories.tsx b/src/useSessionStorageValue/__docs__/example.stories.tsx index 18e4736d8..f7690a39c 100644 --- a/src/useSessionStorageValue/__docs__/example.stories.tsx +++ b/src/useSessionStorageValue/__docs__/example.stories.tsx @@ -2,39 +2,39 @@ import React from 'react'; import { useSessionStorageValue } from '../..'; type ExampleProps = { - /** - * Default value to return in case key not presented in SessionStorage. - */ - defaultValue: string; - /** - * SessionStorage key to manage. - */ - key: string; + /** + * Default value to return in case key not presented in SessionStorage. + */ + defaultValue: string; + /** + * SessionStorage key to manage. + */ + key: string; }; export function Example({ - key = 'react-hookz-ss-test', - defaultValue = '@react-hookz is awesome', + key = 'react-hookz-ss-test', + defaultValue = '@react-hookz is awesome', }: ExampleProps) { - const ssVal = useSessionStorageValue(key, { defaultValue }); + const ssVal = useSessionStorageValue(key, { defaultValue }); - return ( -
-
- Below input value will persist between page reloads and even browser restart as its value is - stored in SessionStorage. -
-
- { - ssVal.set(ev.currentTarget.value); - }} - /> - -
- ); + return ( +
+
+ Below input value will persist between page reloads and even browser restart as its value is + stored in SessionStorage. +
+
+ { + ssVal.set(ev.currentTarget.value); + }} + /> + +
+ ); } diff --git a/src/useSessionStorageValue/__docs__/story.mdx b/src/useSessionStorageValue/__docs__/story.mdx index 634f4c32f..cc9938b75 100644 --- a/src/useSessionStorageValue/__docs__/story.mdx +++ b/src/useSessionStorageValue/__docs__/story.mdx @@ -28,11 +28,11 @@ Manages a single SessionStorage key. #### Example - -
- It is also synchronised between hooks on the same page -
- + +
+ It is also synchronised between hooks on the same page +
+
@@ -41,12 +41,12 @@ Manages a single SessionStorage key. ```ts function useSessionStorageValue< - Type, - Default extends Type = Type, - Initialize extends boolean | undefined = boolean | undefined + Type, + Default extends Type = Type, + Initialize extends boolean | undefined = boolean | undefined >( - key: string, - options?: UseStorageValueOptions + key: string, + options?: UseStorageValueOptions ): UseStorageValueResult; ``` diff --git a/src/useSessionStorageValue/__tests__/dom.ts b/src/useSessionStorageValue/__tests__/dom.ts index b69a39e05..e87439853 100644 --- a/src/useSessionStorageValue/__tests__/dom.ts +++ b/src/useSessionStorageValue/__tests__/dom.ts @@ -2,14 +2,14 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useSessionStorageValue } from '../..'; describe('useSessionStorageValue', () => { - it('should be defined', () => { - expect(useSessionStorageValue).toBeDefined(); - }); + it('should be defined', () => { + expect(useSessionStorageValue).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useSessionStorageValue('foo'); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useSessionStorageValue('foo'); + }); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useSessionStorageValue/__tests__/ssr.ts b/src/useSessionStorageValue/__tests__/ssr.ts index 7473ea869..c851c6a99 100644 --- a/src/useSessionStorageValue/__tests__/ssr.ts +++ b/src/useSessionStorageValue/__tests__/ssr.ts @@ -2,14 +2,14 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useSessionStorageValue } from '../..'; describe('useSessionStorageValue', () => { - it('should be defined', () => { - expect(useSessionStorageValue).toBeDefined(); - }); + it('should be defined', () => { + expect(useSessionStorageValue).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useSessionStorageValue('foo'); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useSessionStorageValue('foo'); + }); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useSessionStorageValue/index.ts b/src/useSessionStorageValue/index.ts index b98f81533..cf6063495 100644 --- a/src/useSessionStorageValue/index.ts +++ b/src/useSessionStorageValue/index.ts @@ -1,47 +1,47 @@ import { - useStorageValue, - type UseStorageValueOptions, - type UseStorageValueResult, + useStorageValue, + type UseStorageValueOptions, + type UseStorageValueResult, } from '../useStorageValue'; import { isBrowser, noop } from '../util/const'; let IS_SESSION_STORAGE_AVAILABLE: boolean; try { - IS_SESSION_STORAGE_AVAILABLE = isBrowser && Boolean(window.sessionStorage); + IS_SESSION_STORAGE_AVAILABLE = isBrowser && Boolean(window.sessionStorage); } catch { - // No need to test as this flag leads to noop behaviour - /* istanbul ignore next */ - IS_SESSION_STORAGE_AVAILABLE = false; + // No need to test as this flag leads to noop behaviour + /* istanbul ignore next */ + IS_SESSION_STORAGE_AVAILABLE = false; } type UseSessionStorageValue = < - Type, - Default extends Type = Type, - Initialize extends boolean | undefined = boolean | undefined + Type, + Default extends Type = Type, + Initialize extends boolean | undefined = boolean | undefined >( - key: string, - options?: UseStorageValueOptions + key: string, + options?: UseStorageValueOptions ) => UseStorageValueResult; /** * Manages a single sessionStorage key. */ export const useSessionStorageValue: UseSessionStorageValue = IS_SESSION_STORAGE_AVAILABLE - ? (key, options) => { - return useStorageValue(sessionStorage, key, options); - } - : < - Type, - Default extends Type = Type, - Initialize extends boolean | undefined = boolean | undefined - >( - _key: string, - _options?: UseStorageValueOptions - ): UseStorageValueResult => { - if (isBrowser && process.env.NODE_ENV === 'development') { - console.warn('SessionStorage is not available in this environment'); - } + ? (key, options) => { + return useStorageValue(sessionStorage, key, options); + } + : < + Type, + Default extends Type = Type, + Initialize extends boolean | undefined = boolean | undefined + >( + _key: string, + _options?: UseStorageValueOptions + ): UseStorageValueResult => { + if (isBrowser && process.env.NODE_ENV === 'development') { + console.warn('SessionStorage is not available in this environment'); + } - return { value: undefined as Type, set: noop, remove: noop, fetch: noop }; - }; + return { value: undefined as Type, set: noop, remove: noop, fetch: noop }; + }; diff --git a/src/useSet/__docs__/example.stories.tsx b/src/useSet/__docs__/example.stories.tsx index 7f313cc51..410ade806 100644 --- a/src/useSet/__docs__/example.stories.tsx +++ b/src/useSet/__docs__/example.stories.tsx @@ -3,33 +3,33 @@ import * as React from 'react'; import { useSet } from '../..'; export function Example() { - const set = useSet(['@react-hooks', 'is awesome']); + const set = useSet(['@react-hooks', 'is awesome']); - return ( -
- - - - -
-
{JSON.stringify([...set], null, 2)}
-
- ); + return ( +
+ + + + +
+
{JSON.stringify([...set], null, 2)}
+
+ ); } diff --git a/src/useSet/__docs__/story.mdx b/src/useSet/__docs__/story.mdx index a3546f734..5f023b5fe 100644 --- a/src/useSet/__docs__/story.mdx +++ b/src/useSet/__docs__/story.mdx @@ -17,7 +17,7 @@ Tracks the state of a `Set`. #### Example - + ## Reference diff --git a/src/useSet/__tests__/dom.ts b/src/useSet/__tests__/dom.ts index 8d64f74d8..7364e3532 100644 --- a/src/useSet/__tests__/dom.ts +++ b/src/useSet/__tests__/dom.ts @@ -2,75 +2,75 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { useSet } from '../..'; describe('useSet', () => { - it('should be defined', () => { - expect(useSet).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useSet()); - expect(result.error).toBeUndefined(); - }); - - it('should return a Set instance with altered add, clear and delete methods', () => { - const { result } = renderHook(() => useSet()); - expect(result.current).toBeInstanceOf(Set); - expect(result.current.add).not.toBe(Set.prototype.add); - expect(result.current.clear).not.toBe(Set.prototype.clear); - expect(result.current.delete).not.toBe(Set.prototype.delete); - }); - - it('should accept initial values', () => { - const { result } = renderHook(() => useSet([1, 2, 3])); - expect(result.current.has(1)).toBe(true); - expect(result.current.has(2)).toBe(true); - expect(result.current.has(3)).toBe(true); - expect(result.current.size).toBe(3); - }); - - it('`add` should invoke original method and rerender component', async () => { - const spy = jest.spyOn(Set.prototype, 'add'); - let i = 0; - const { result, waitForNextUpdate } = renderHook(() => [++i, useSet()] as const); - - await act(async () => { - expect(result.current[1].add(1)).toBe(result.current[1]); - expect(spy).toHaveBeenCalledWith(1); - await waitForNextUpdate(); - }); - - expect(result.current[0]).toBe(2); - - spy.mockRestore(); - }); - - it('`clear` should invoke original method and rerender component', async () => { - const spy = jest.spyOn(Set.prototype, 'clear'); - let i = 0; - const { result, waitForNextUpdate } = renderHook(() => [++i, useSet()] as const); - - await act(async () => { - result.current[1].clear(); - await waitForNextUpdate(); - }); - - expect(result.current[0]).toBe(2); - - spy.mockRestore(); - }); - - it('`delete` should invoke original method and rerender component', async () => { - const spy = jest.spyOn(Set.prototype, 'delete'); - let i = 0; - const { result, waitForNextUpdate } = renderHook(() => [++i, useSet([1])] as const); - - await act(async () => { - expect(result.current[1].delete(1)).toBe(true); - expect(spy).toHaveBeenCalledWith(1); - await waitForNextUpdate(); - }); - - expect(result.current[0]).toBe(2); - - spy.mockRestore(); - }); + it('should be defined', () => { + expect(useSet).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useSet()); + expect(result.error).toBeUndefined(); + }); + + it('should return a Set instance with altered add, clear and delete methods', () => { + const { result } = renderHook(() => useSet()); + expect(result.current).toBeInstanceOf(Set); + expect(result.current.add).not.toBe(Set.prototype.add); + expect(result.current.clear).not.toBe(Set.prototype.clear); + expect(result.current.delete).not.toBe(Set.prototype.delete); + }); + + it('should accept initial values', () => { + const { result } = renderHook(() => useSet([1, 2, 3])); + expect(result.current.has(1)).toBe(true); + expect(result.current.has(2)).toBe(true); + expect(result.current.has(3)).toBe(true); + expect(result.current.size).toBe(3); + }); + + it('`add` should invoke original method and rerender component', async () => { + const spy = jest.spyOn(Set.prototype, 'add'); + let i = 0; + const { result, waitForNextUpdate } = renderHook(() => [++i, useSet()] as const); + + await act(async () => { + expect(result.current[1].add(1)).toBe(result.current[1]); + expect(spy).toHaveBeenCalledWith(1); + await waitForNextUpdate(); + }); + + expect(result.current[0]).toBe(2); + + spy.mockRestore(); + }); + + it('`clear` should invoke original method and rerender component', async () => { + const spy = jest.spyOn(Set.prototype, 'clear'); + let i = 0; + const { result, waitForNextUpdate } = renderHook(() => [++i, useSet()] as const); + + await act(async () => { + result.current[1].clear(); + await waitForNextUpdate(); + }); + + expect(result.current[0]).toBe(2); + + spy.mockRestore(); + }); + + it('`delete` should invoke original method and rerender component', async () => { + const spy = jest.spyOn(Set.prototype, 'delete'); + let i = 0; + const { result, waitForNextUpdate } = renderHook(() => [++i, useSet([1])] as const); + + await act(async () => { + expect(result.current[1].delete(1)).toBe(true); + expect(spy).toHaveBeenCalledWith(1); + await waitForNextUpdate(); + }); + + expect(result.current[0]).toBe(2); + + spy.mockRestore(); + }); }); diff --git a/src/useSet/__tests__/ssr.ts b/src/useSet/__tests__/ssr.ts index e748333af..3165a4c95 100644 --- a/src/useSet/__tests__/ssr.ts +++ b/src/useSet/__tests__/ssr.ts @@ -3,72 +3,72 @@ import { act } from '@testing-library/react-hooks/dom'; import { useSet } from '../..'; describe('useSet', () => { - it('should be defined', () => { - expect(useSet).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useSet()); - expect(result.error).toBeUndefined(); - }); - - it('should return a Set instance with altered add, clear and delete methods', () => { - const { result } = renderHook(() => useSet()); - expect(result.current).toBeInstanceOf(Set); - expect(result.current.add).not.toBe(Set.prototype.add); - expect(result.current.clear).not.toBe(Set.prototype.clear); - expect(result.current.delete).not.toBe(Set.prototype.delete); - }); - - it('should accept initial values', () => { - const { result } = renderHook(() => useSet([1, 2, 3])); - expect(result.current.has(1)).toBe(true); - expect(result.current.has(2)).toBe(true); - expect(result.current.has(3)).toBe(true); - expect(result.current.size).toBe(3); - }); - - it('`add` should invoke original method and rerender component', () => { - const spy = jest.spyOn(Set.prototype, 'add'); - let i = 0; - const { result } = renderHook(() => [++i, useSet()] as const); - - act(() => { - expect(result.current[1].add(1)).toBe(result.current[1]); - expect(spy).toHaveBeenCalledWith(1); - }); - - expect(result.current[0]).toBe(1); - - spy.mockRestore(); - }); - - it('`clear` should invoke original method and rerender component', () => { - const spy = jest.spyOn(Set.prototype, 'clear'); - let i = 0; - const { result } = renderHook(() => [++i, useSet()] as const); - - act(() => { - result.current[1].clear(); - }); - - expect(result.current[0]).toBe(1); - - spy.mockRestore(); - }); - - it('`delete` should invoke original method and rerender component', () => { - const spy = jest.spyOn(Set.prototype, 'delete'); - let i = 0; - const { result } = renderHook(() => [++i, useSet([1])] as const); - - act(() => { - expect(result.current[1].delete(1)).toBe(true); - expect(spy).toHaveBeenCalledWith(1); - }); - - expect(result.current[0]).toBe(1); - - spy.mockRestore(); - }); + it('should be defined', () => { + expect(useSet).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useSet()); + expect(result.error).toBeUndefined(); + }); + + it('should return a Set instance with altered add, clear and delete methods', () => { + const { result } = renderHook(() => useSet()); + expect(result.current).toBeInstanceOf(Set); + expect(result.current.add).not.toBe(Set.prototype.add); + expect(result.current.clear).not.toBe(Set.prototype.clear); + expect(result.current.delete).not.toBe(Set.prototype.delete); + }); + + it('should accept initial values', () => { + const { result } = renderHook(() => useSet([1, 2, 3])); + expect(result.current.has(1)).toBe(true); + expect(result.current.has(2)).toBe(true); + expect(result.current.has(3)).toBe(true); + expect(result.current.size).toBe(3); + }); + + it('`add` should invoke original method and rerender component', () => { + const spy = jest.spyOn(Set.prototype, 'add'); + let i = 0; + const { result } = renderHook(() => [++i, useSet()] as const); + + act(() => { + expect(result.current[1].add(1)).toBe(result.current[1]); + expect(spy).toHaveBeenCalledWith(1); + }); + + expect(result.current[0]).toBe(1); + + spy.mockRestore(); + }); + + it('`clear` should invoke original method and rerender component', () => { + const spy = jest.spyOn(Set.prototype, 'clear'); + let i = 0; + const { result } = renderHook(() => [++i, useSet()] as const); + + act(() => { + result.current[1].clear(); + }); + + expect(result.current[0]).toBe(1); + + spy.mockRestore(); + }); + + it('`delete` should invoke original method and rerender component', () => { + const spy = jest.spyOn(Set.prototype, 'delete'); + let i = 0; + const { result } = renderHook(() => [++i, useSet([1])] as const); + + act(() => { + expect(result.current[1].delete(1)).toBe(true); + expect(spy).toHaveBeenCalledWith(1); + }); + + expect(result.current[0]).toBe(1); + + spy.mockRestore(); + }); }); diff --git a/src/useSet/index.ts b/src/useSet/index.ts index cdc3ac219..2e7075879 100644 --- a/src/useSet/index.ts +++ b/src/useSet/index.ts @@ -10,32 +10,32 @@ const proto = Set.prototype; */ export function useSet(values?: readonly T[] | null): Set { - const setRef = useRef>(); - const rerender = useRerender(); + const setRef = useRef>(); + const rerender = useRerender(); - if (!setRef.current) { - const set = new Set(values); + if (!setRef.current) { + const set = new Set(values); - setRef.current = set; + setRef.current = set; - set.add = (...args) => { - proto.add.apply(set, args); - rerender(); - return set; - }; + set.add = (...args) => { + proto.add.apply(set, args); + rerender(); + return set; + }; - set.clear = (...args) => { - proto.clear.apply(set, args); - rerender(); - }; + set.clear = (...args) => { + proto.clear.apply(set, args); + rerender(); + }; - set.delete = (...args) => { - const res = proto.delete.apply(set, args); - rerender(); + set.delete = (...args) => { + const res = proto.delete.apply(set, args); + rerender(); - return res; - }; - } + return res; + }; + } - return setRef.current; + return setRef.current; } diff --git a/src/useStorageValue/__tests__/dom.ts b/src/useStorageValue/__tests__/dom.ts index cccde97a5..3e94d4182 100644 --- a/src/useStorageValue/__tests__/dom.ts +++ b/src/useStorageValue/__tests__/dom.ts @@ -3,331 +3,331 @@ import { newStorage } from './misc'; import { useStorageValue } from '..'; describe('useStorageValue', () => { - it('should be defined', () => { - expect(useStorageValue).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useStorageValue(newStorage(), 'foo')); - - expect(result.error).toBeUndefined(); - }); - - it('should action methods should be stable between renders', () => { - const { result, rerender } = renderHook(() => useStorageValue(newStorage(), 'foo')); - - rerender(); - act(() => { - result.current.set('bar'); - }); - rerender(); - - type ResultType = typeof result.current; - - expect((result.all[0] as ResultType).set).toBe(result.current.set); - expect((result.all[0] as ResultType).fetch).toBe(result.current.fetch); - expect((result.all[0] as ResultType).remove).toBe(result.current.remove); - }); - - it('should fetch value from storage only on init', () => { - const storage = newStorage((key) => `"${key}"`); - const { result, rerender } = renderHook(() => useStorageValue(storage, 'foo')); - - expect(result.current.value).toBe('foo'); - expect(storage.getItem).toHaveBeenCalledWith('foo'); - - rerender(); - rerender(); - rerender(); - - expect(storage.getItem).toHaveBeenCalledTimes(1); - }); - - it('should pass value through JSON.parse during fetch', () => { - const JSONParseSpy = jest.spyOn(JSON, 'parse'); - const storage = newStorage((key) => `"${key}"`); - const { result } = renderHook(() => useStorageValue(storage, 'foo')); - - expect(result.current.value).toBe('foo'); - expect(JSONParseSpy).toHaveBeenCalledWith('"foo"'); - - JSONParseSpy.mockRestore(); - }); - - it('should yield default value in case storage returned null during fetch', () => { - const { result } = renderHook(() => - useStorageValue(newStorage(), 'foo', { defaultValue: 'defaultValue' }) - ); - - expect(result.current.value).toBe('defaultValue'); - }); - - it('should yield default value and console.warn in case storage returned corrupted JSON', () => { - const warnSpy = jest.spyOn(console, 'warn').mockImplementationOnce(() => {}); - const { result } = renderHook(() => - useStorageValue( - newStorage(() => 'corrupted JSON'), - 'foo', - { defaultValue: 'defaultValue' } - ) - ); - - expect(result.current.value).toBe('defaultValue'); - expect(warnSpy.mock.calls[0][0]).toBeInstanceOf(SyntaxError); - - warnSpy.mockRestore(); - }); - - it('should not fetch value on first render in case `initializeWithValue` options is set to false', () => { - const { result } = renderHook(() => - useStorageValue( - newStorage(() => '"bar"'), - 'foo', - { initializeWithValue: false } - ) - ); - - // @ts-expect-error invalid typings of testing library - expect(result.all[0].value).toBe(undefined); - // @ts-expect-error invalid typings of testing library - expect(result.all[1].value).toBe('bar'); - }); - - it('should fetch value on first render in case `initializeWithValue` options is set to true', () => { - const { result } = renderHook(() => - useStorageValue( - newStorage(() => '"bar"'), - 'foo', - { initializeWithValue: true } - ) - ); - // @ts-expect-error invalid typings of testing library - expect(result.all[0].value).toBe('bar'); - }); - - it('should set storage value on .set() call', () => { - const { result } = renderHook(() => useStorageValue(newStorage(), 'foo')); - - expect(result.current.value).toBe(null); - act(() => { - result.current.set('bar'); - }); - expect(result.current.value).toBe('bar'); - - const spySetter = jest.fn(() => 'baz'); - act(() => { - result.current.set(spySetter); - }); - expect(result.current.value).toBe('baz'); - expect(spySetter).toHaveBeenCalledWith('bar'); - }); - - it('should call JSON.stringify on setState call', () => { - const JSONStringifySpy = jest.spyOn(JSON, 'stringify'); - const { result } = renderHook(() => useStorageValue(newStorage(), 'foo')); - - expect(result.current.value).toBe(null); - act(() => { - result.current.set('bar'); - }); - expect(result.current.value).toBe('bar'); - expect(JSONStringifySpy).toHaveBeenCalledWith('bar'); - JSONStringifySpy.mockRestore(); - }); - - it('should not store null or data that cannot be processed by JSON serializer', () => { - const { result } = renderHook(() => - useStorageValue( - newStorage(() => '"bar"'), - 'foo', - { defaultValue: 'default value' } - ) - ); - - const invalidData: { a?: unknown } = {}; - invalidData.a = { b: invalidData }; - - expect(result.current.value).toBe('bar'); - act(() => { - // @ts-expect-error testing inappropriate use - result.current.set(null); - }); - expect(result.current.value).toBe('bar'); - }); - - it('should call storage`s removeItem on .remove() call', () => { - const storage = newStorage(); - const { result } = renderHook(() => useStorageValue(storage, 'foo')); - - act(() => { - result.current.remove(); - }); - expect(storage.removeItem).toHaveBeenCalledWith('foo'); - }); - - it('should set state to default value on item remove', () => { - const { result } = renderHook(() => - useStorageValue( - newStorage(() => '"bar"'), - 'foo', - { defaultValue: 'default value' } - ) - ); - - expect(result.current.value).toBe('bar'); - act(() => { - result.current.remove(); - }); - expect(result.current.value).toBe('default value'); - }); - - it('should refetch value from store on .fetch() call', () => { - const storage = newStorage(() => '"bar"'); - const { result } = renderHook(() => - useStorageValue(storage, 'foo', { defaultValue: 'default value' }) - ); - - expect(storage.getItem).toHaveBeenCalledTimes(1); - expect(result.current.value).toBe('bar'); - storage.getItem.mockImplementationOnce(() => '"baz"'); - - act(() => { - result.current.fetch(); - }); - - expect(storage.getItem).toHaveBeenCalledTimes(2); - expect(result.current.value).toBe('baz'); - }); - - it('should refetch value on key change', () => { - const storage = newStorage((k) => `"${k}"`); - const { result, rerender } = renderHook( - ({ key }) => useStorageValue(storage, key, { defaultValue: 'default value' }), - { initialProps: { key: 'foo' } } - ); - - expect(result.current.value).toBe('foo'); - rerender({ key: 'bar' }); - expect(result.current.value).toBe('bar'); - }); - - it('should use custom stringify option', () => { - const storage = newStorage(); - const { result } = renderHook(() => - useStorageValue(storage, 'foo', { - stringify(data) { - return data.map((num) => num.toString(16)).join(':'); - }, - parse(str, fallback) { - if (str === null) return fallback; - - if (str === '') return []; - - return str.split(':').map((num) => Number.parseInt(num, 16)); - }, - }) - ); - - expect(result.current.value).toBe(null); - act(() => { - result.current.set([1, 2, 3]); - }); - expect(storage.setItem).toHaveBeenCalledWith('foo', '1:2:3'); - }); - - it('should use custom parse option', () => { - const storage = newStorage(); - storage.getItem.mockImplementationOnce(() => '1:2:3'); - const { result } = renderHook(() => - useStorageValue(storage, 'foo', { - stringify(data) { - return data.map((num) => num.toString(16)).join(':'); - }, - parse(str, fallback) { - if (str === null) return fallback; - - if (str === '') return []; - - return str.split(':').map((num) => Number.parseInt(num, 16)); - }, - }) - ); - expect(result.current.value).toEqual([1, 2, 3]); - }); - - describe('should handle window`s `storage` event', () => { - it('should update state if tracked key is updated', () => { - const { result } = renderHook(() => useStorageValue(localStorage, 'foo')); - expect(result.current.value).toBe(null); - - localStorage.setItem('foo', 'bar'); - act(() => { - window.dispatchEvent( - new StorageEvent('storage', { key: 'foo', storageArea: localStorage, newValue: '"foo"' }) - ); - }); - - expect(result.current.value).toBe('foo'); - localStorage.removeItem('foo'); - }); - - it('should not update data on event storage or key mismatch', () => { - const { result } = renderHook(() => useStorageValue(localStorage, 'foo')); - expect(result.current.value).toBe(null); - - act(() => { - window.dispatchEvent( - new StorageEvent('storage', { - key: 'foo', - storageArea: sessionStorage, - newValue: '"foo"', - }) - ); - }); - expect(result.current.value).toBe(null); - - act(() => { - window.dispatchEvent( - new StorageEvent('storage', { - key: 'bar', - storageArea: localStorage, - newValue: 'foo', - }) - ); - }); - expect(result.current.value).toBe(null); - - localStorage.removeItem('foo'); - }); - }); - - describe('synchronisation', () => { - it('should update state of all hooks with the same key in same storage', () => { - const { result: res } = renderHook(() => useStorageValue(localStorage, 'foo')); - const { result: res1 } = renderHook(() => useStorageValue(localStorage, 'foo')); - - expect(res.current.value).toBe(null); - expect(res1.current.value).toBe(null); - - act(() => { - res.current.set('bar'); - }); - expect(res.current.value).toBe('bar'); - expect(res1.current.value).toBe('bar'); - - act(() => { - res.current.remove(); - }); - expect(res.current.value).toBe(null); - expect(res1.current.value).toBe(null); - - localStorage.setItem('foo', '"123"'); - act(() => { - res.current.fetch(); - }); - expect(res.current.value).toBe('123'); - expect(res1.current.value).toBe('123'); - localStorage.removeItem('foo'); - }); - }); + it('should be defined', () => { + expect(useStorageValue).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useStorageValue(newStorage(), 'foo')); + + expect(result.error).toBeUndefined(); + }); + + it('should action methods should be stable between renders', () => { + const { result, rerender } = renderHook(() => useStorageValue(newStorage(), 'foo')); + + rerender(); + act(() => { + result.current.set('bar'); + }); + rerender(); + + type ResultType = typeof result.current; + + expect((result.all[0] as ResultType).set).toBe(result.current.set); + expect((result.all[0] as ResultType).fetch).toBe(result.current.fetch); + expect((result.all[0] as ResultType).remove).toBe(result.current.remove); + }); + + it('should fetch value from storage only on init', () => { + const storage = newStorage((key) => `"${key}"`); + const { result, rerender } = renderHook(() => useStorageValue(storage, 'foo')); + + expect(result.current.value).toBe('foo'); + expect(storage.getItem).toHaveBeenCalledWith('foo'); + + rerender(); + rerender(); + rerender(); + + expect(storage.getItem).toHaveBeenCalledTimes(1); + }); + + it('should pass value through JSON.parse during fetch', () => { + const JSONParseSpy = jest.spyOn(JSON, 'parse'); + const storage = newStorage((key) => `"${key}"`); + const { result } = renderHook(() => useStorageValue(storage, 'foo')); + + expect(result.current.value).toBe('foo'); + expect(JSONParseSpy).toHaveBeenCalledWith('"foo"'); + + JSONParseSpy.mockRestore(); + }); + + it('should yield default value in case storage returned null during fetch', () => { + const { result } = renderHook(() => + useStorageValue(newStorage(), 'foo', { defaultValue: 'defaultValue' }) + ); + + expect(result.current.value).toBe('defaultValue'); + }); + + it('should yield default value and console.warn in case storage returned corrupted JSON', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementationOnce(() => {}); + const { result } = renderHook(() => + useStorageValue( + newStorage(() => 'corrupted JSON'), + 'foo', + { defaultValue: 'defaultValue' } + ) + ); + + expect(result.current.value).toBe('defaultValue'); + expect(warnSpy.mock.calls[0][0]).toBeInstanceOf(SyntaxError); + + warnSpy.mockRestore(); + }); + + it('should not fetch value on first render in case `initializeWithValue` options is set to false', () => { + const { result } = renderHook(() => + useStorageValue( + newStorage(() => '"bar"'), + 'foo', + { initializeWithValue: false } + ) + ); + + // @ts-expect-error invalid typings of testing library + expect(result.all[0].value).toBe(undefined); + // @ts-expect-error invalid typings of testing library + expect(result.all[1].value).toBe('bar'); + }); + + it('should fetch value on first render in case `initializeWithValue` options is set to true', () => { + const { result } = renderHook(() => + useStorageValue( + newStorage(() => '"bar"'), + 'foo', + { initializeWithValue: true } + ) + ); + // @ts-expect-error invalid typings of testing library + expect(result.all[0].value).toBe('bar'); + }); + + it('should set storage value on .set() call', () => { + const { result } = renderHook(() => useStorageValue(newStorage(), 'foo')); + + expect(result.current.value).toBe(null); + act(() => { + result.current.set('bar'); + }); + expect(result.current.value).toBe('bar'); + + const spySetter = jest.fn(() => 'baz'); + act(() => { + result.current.set(spySetter); + }); + expect(result.current.value).toBe('baz'); + expect(spySetter).toHaveBeenCalledWith('bar'); + }); + + it('should call JSON.stringify on setState call', () => { + const JSONStringifySpy = jest.spyOn(JSON, 'stringify'); + const { result } = renderHook(() => useStorageValue(newStorage(), 'foo')); + + expect(result.current.value).toBe(null); + act(() => { + result.current.set('bar'); + }); + expect(result.current.value).toBe('bar'); + expect(JSONStringifySpy).toHaveBeenCalledWith('bar'); + JSONStringifySpy.mockRestore(); + }); + + it('should not store null or data that cannot be processed by JSON serializer', () => { + const { result } = renderHook(() => + useStorageValue( + newStorage(() => '"bar"'), + 'foo', + { defaultValue: 'default value' } + ) + ); + + const invalidData: { a?: unknown } = {}; + invalidData.a = { b: invalidData }; + + expect(result.current.value).toBe('bar'); + act(() => { + // @ts-expect-error testing inappropriate use + result.current.set(null); + }); + expect(result.current.value).toBe('bar'); + }); + + it('should call storage`s removeItem on .remove() call', () => { + const storage = newStorage(); + const { result } = renderHook(() => useStorageValue(storage, 'foo')); + + act(() => { + result.current.remove(); + }); + expect(storage.removeItem).toHaveBeenCalledWith('foo'); + }); + + it('should set state to default value on item remove', () => { + const { result } = renderHook(() => + useStorageValue( + newStorage(() => '"bar"'), + 'foo', + { defaultValue: 'default value' } + ) + ); + + expect(result.current.value).toBe('bar'); + act(() => { + result.current.remove(); + }); + expect(result.current.value).toBe('default value'); + }); + + it('should refetch value from store on .fetch() call', () => { + const storage = newStorage(() => '"bar"'); + const { result } = renderHook(() => + useStorageValue(storage, 'foo', { defaultValue: 'default value' }) + ); + + expect(storage.getItem).toHaveBeenCalledTimes(1); + expect(result.current.value).toBe('bar'); + storage.getItem.mockImplementationOnce(() => '"baz"'); + + act(() => { + result.current.fetch(); + }); + + expect(storage.getItem).toHaveBeenCalledTimes(2); + expect(result.current.value).toBe('baz'); + }); + + it('should refetch value on key change', () => { + const storage = newStorage((k) => `"${k}"`); + const { result, rerender } = renderHook( + ({ key }) => useStorageValue(storage, key, { defaultValue: 'default value' }), + { initialProps: { key: 'foo' } } + ); + + expect(result.current.value).toBe('foo'); + rerender({ key: 'bar' }); + expect(result.current.value).toBe('bar'); + }); + + it('should use custom stringify option', () => { + const storage = newStorage(); + const { result } = renderHook(() => + useStorageValue(storage, 'foo', { + stringify(data) { + return data.map((num) => num.toString(16)).join(':'); + }, + parse(str, fallback) { + if (str === null) return fallback; + + if (str === '') return []; + + return str.split(':').map((num) => Number.parseInt(num, 16)); + }, + }) + ); + + expect(result.current.value).toBe(null); + act(() => { + result.current.set([1, 2, 3]); + }); + expect(storage.setItem).toHaveBeenCalledWith('foo', '1:2:3'); + }); + + it('should use custom parse option', () => { + const storage = newStorage(); + storage.getItem.mockImplementationOnce(() => '1:2:3'); + const { result } = renderHook(() => + useStorageValue(storage, 'foo', { + stringify(data) { + return data.map((num) => num.toString(16)).join(':'); + }, + parse(str, fallback) { + if (str === null) return fallback; + + if (str === '') return []; + + return str.split(':').map((num) => Number.parseInt(num, 16)); + }, + }) + ); + expect(result.current.value).toEqual([1, 2, 3]); + }); + + describe('should handle window`s `storage` event', () => { + it('should update state if tracked key is updated', () => { + const { result } = renderHook(() => useStorageValue(localStorage, 'foo')); + expect(result.current.value).toBe(null); + + localStorage.setItem('foo', 'bar'); + act(() => { + window.dispatchEvent( + new StorageEvent('storage', { key: 'foo', storageArea: localStorage, newValue: '"foo"' }) + ); + }); + + expect(result.current.value).toBe('foo'); + localStorage.removeItem('foo'); + }); + + it('should not update data on event storage or key mismatch', () => { + const { result } = renderHook(() => useStorageValue(localStorage, 'foo')); + expect(result.current.value).toBe(null); + + act(() => { + window.dispatchEvent( + new StorageEvent('storage', { + key: 'foo', + storageArea: sessionStorage, + newValue: '"foo"', + }) + ); + }); + expect(result.current.value).toBe(null); + + act(() => { + window.dispatchEvent( + new StorageEvent('storage', { + key: 'bar', + storageArea: localStorage, + newValue: 'foo', + }) + ); + }); + expect(result.current.value).toBe(null); + + localStorage.removeItem('foo'); + }); + }); + + describe('synchronisation', () => { + it('should update state of all hooks with the same key in same storage', () => { + const { result: res } = renderHook(() => useStorageValue(localStorage, 'foo')); + const { result: res1 } = renderHook(() => useStorageValue(localStorage, 'foo')); + + expect(res.current.value).toBe(null); + expect(res1.current.value).toBe(null); + + act(() => { + res.current.set('bar'); + }); + expect(res.current.value).toBe('bar'); + expect(res1.current.value).toBe('bar'); + + act(() => { + res.current.remove(); + }); + expect(res.current.value).toBe(null); + expect(res1.current.value).toBe(null); + + localStorage.setItem('foo', '"123"'); + act(() => { + res.current.fetch(); + }); + expect(res.current.value).toBe('123'); + expect(res1.current.value).toBe('123'); + localStorage.removeItem('foo'); + }); + }); }); diff --git a/src/useStorageValue/__tests__/misc.ts b/src/useStorageValue/__tests__/misc.ts index 699596850..836106a29 100644 --- a/src/useStorageValue/__tests__/misc.ts +++ b/src/useStorageValue/__tests__/misc.ts @@ -1,13 +1,13 @@ import Mocked = jest.Mocked; export const newStorage = ( - get: Storage['getItem'] = () => null, - set: Storage['setItem'] = () => {}, - remove: Storage['removeItem'] = () => {} + get: Storage['getItem'] = () => null, + set: Storage['setItem'] = () => {}, + remove: Storage['removeItem'] = () => {} ) => { - return { - getItem: jest.fn(get), - setItem: jest.fn(set), - removeItem: jest.fn(remove), - } as unknown as Mocked; + return { + getItem: jest.fn(get), + setItem: jest.fn(set), + removeItem: jest.fn(remove), + } as unknown as Mocked; }; diff --git a/src/useStorageValue/__tests__/ssr.ts b/src/useStorageValue/__tests__/ssr.ts index 867e9d32d..bcfad707d 100644 --- a/src/useStorageValue/__tests__/ssr.ts +++ b/src/useStorageValue/__tests__/ssr.ts @@ -3,80 +3,80 @@ import { newStorage } from './misc'; import { useStorageValue } from '..'; describe('useStorageValue', () => { - it('should be defined', () => { - expect(useStorageValue).toBeDefined(); - }); + it('should be defined', () => { + expect(useStorageValue).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useStorageValue(newStorage(), 'foo')); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useStorageValue(newStorage(), 'foo')); + expect(result.error).toBeUndefined(); + }); - describe('if initializeWithValue set to false', () => { - it('should not fetch value from storage on init', () => { - const storage = newStorage(); - const { result } = renderHook(() => - useStorageValue(storage, 'foo', { initializeWithValue: false }) - ); + describe('if initializeWithValue set to false', () => { + it('should not fetch value from storage on init', () => { + const storage = newStorage(); + const { result } = renderHook(() => + useStorageValue(storage, 'foo', { initializeWithValue: false }) + ); - expect(result.current.value).toBe(undefined); - expect(storage.getItem).not.toHaveBeenCalled(); - }); + expect(result.current.value).toBe(undefined); + expect(storage.getItem).not.toHaveBeenCalled(); + }); - it('should not fetch value from storage on .fetch() call', () => { - const storage = newStorage(); - const { result } = renderHook(() => - useStorageValue(storage, 'foo', { initializeWithValue: false }) - ); + it('should not fetch value from storage on .fetch() call', () => { + const storage = newStorage(); + const { result } = renderHook(() => + useStorageValue(storage, 'foo', { initializeWithValue: false }) + ); - expect(result.current.value).toBe(undefined); - act(() => { - result.current.fetch(); - }); - expect(result.current.value).toBe(undefined); - expect(storage.getItem).not.toHaveBeenCalled(); - }); + expect(result.current.value).toBe(undefined); + act(() => { + result.current.fetch(); + }); + expect(result.current.value).toBe(undefined); + expect(storage.getItem).not.toHaveBeenCalled(); + }); - it('should not set storage value on .set() call', () => { - const storage = newStorage(); - const { result } = renderHook(() => - useStorageValue(storage, 'foo', { initializeWithValue: false }) - ); + it('should not set storage value on .set() call', () => { + const storage = newStorage(); + const { result } = renderHook(() => + useStorageValue(storage, 'foo', { initializeWithValue: false }) + ); - expect(result.current.value).toBe(undefined); - act(() => { - result.current.set('bar'); - }); - expect(result.current.value).toBe(undefined); - expect(storage.setItem).not.toHaveBeenCalled(); - }); + expect(result.current.value).toBe(undefined); + act(() => { + result.current.set('bar'); + }); + expect(result.current.value).toBe(undefined); + expect(storage.setItem).not.toHaveBeenCalled(); + }); - it('should not call storage`s removeItem on .remove() call', () => { - const storage = newStorage(); - const { result } = renderHook(() => - useStorageValue(storage, 'foo', { initializeWithValue: false }) - ); + it('should not call storage`s removeItem on .remove() call', () => { + const storage = newStorage(); + const { result } = renderHook(() => + useStorageValue(storage, 'foo', { initializeWithValue: false }) + ); - act(() => { - result.current.remove(); - }); - expect(storage.removeItem).not.toHaveBeenCalled(); - }); + act(() => { + result.current.remove(); + }); + expect(storage.removeItem).not.toHaveBeenCalled(); + }); - it('should not set state to default value on item remove', () => { - const storage = newStorage(() => '"bar"'); - const { result } = renderHook(() => - useStorageValue(storage, 'foo', { - defaultValue: 'default value', - initializeWithValue: false, - }) - ); + it('should not set state to default value on item remove', () => { + const storage = newStorage(() => '"bar"'); + const { result } = renderHook(() => + useStorageValue(storage, 'foo', { + defaultValue: 'default value', + initializeWithValue: false, + }) + ); - expect(result.current.value).toBe(undefined); - act(() => { - result.current.remove(); - }); - expect(result.current.value).toBe(undefined); - }); - }); + expect(result.current.value).toBe(undefined); + act(() => { + result.current.remove(); + }); + expect(result.current.value).toBe(undefined); + }); + }); }); diff --git a/src/useStorageValue/index.ts b/src/useStorageValue/index.ts index 2fb74f046..561f74fdb 100644 --- a/src/useStorageValue/index.ts +++ b/src/useStorageValue/index.ts @@ -10,295 +10,295 @@ import { type NextState, resolveHookState } from '../util/resolveHookState'; const storageListeners = new Map>>(); const invokeStorageKeyListeners = ( - s: Storage, - key: string, - value: string | null, - skipListener?: CallableFunction + s: Storage, + key: string, + value: string | null, + skipListener?: CallableFunction ) => { - storageListeners - .get(s) - ?.get(key) - ?.forEach((listener) => { - if (listener !== skipListener) { - listener(value); - } - }); + storageListeners + .get(s) + ?.get(key) + ?.forEach((listener) => { + if (listener !== skipListener) { + listener(value); + } + }); }; const storageEventHandler = (evt: StorageEvent) => { - if (evt.storageArea && evt.key && evt.newValue) { - invokeStorageKeyListeners(evt.storageArea, evt.key, evt.newValue); - } + if (evt.storageArea && evt.key && evt.newValue) { + invokeStorageKeyListeners(evt.storageArea, evt.key, evt.newValue); + } }; const addStorageListener = (s: Storage, key: string, listener: CallableFunction) => { - // In case of first listener added within browser environment we - // want to bind single storage event handler - if (isBrowser && storageListeners.size === 0) { - on(window, 'storage', storageEventHandler, { passive: true }); - } - - let keys = storageListeners.get(s); - if (!keys) { - keys = new Map(); - storageListeners.set(s, keys); - } - - let listeners = keys.get(key); - if (!listeners) { - listeners = new Set(); - keys.set(key, listeners); - } - - listeners.add(listener); + // In case of first listener added within browser environment we + // want to bind single storage event handler + if (isBrowser && storageListeners.size === 0) { + on(window, 'storage', storageEventHandler, { passive: true }); + } + + let keys = storageListeners.get(s); + if (!keys) { + keys = new Map(); + storageListeners.set(s, keys); + } + + let listeners = keys.get(key); + if (!listeners) { + listeners = new Set(); + keys.set(key, listeners); + } + + listeners.add(listener); }; const removeStorageListener = (s: Storage, key: string, listener: CallableFunction) => { - const keys = storageListeners.get(s); - /* istanbul ignore next */ - if (!keys) { - return; - } - - const listeners = keys.get(key); - /* istanbul ignore next */ - if (!listeners) { - return; - } - - listeners.delete(listener); - - if (!listeners.size) { - keys.delete(key); - } - - if (!keys.size) { - storageListeners.delete(s); - } - - // Unbind storage event handler in browser environment in case there is no - // storage keys listeners left - if (isBrowser && !storageListeners.size) { - off(window, 'storage', storageEventHandler); - } + const keys = storageListeners.get(s); + /* istanbul ignore next */ + if (!keys) { + return; + } + + const listeners = keys.get(key); + /* istanbul ignore next */ + if (!listeners) { + return; + } + + listeners.delete(listener); + + if (!listeners.size) { + keys.delete(key); + } + + if (!keys.size) { + storageListeners.delete(s); + } + + // Unbind storage event handler in browser environment in case there is no + // storage keys listeners left + if (isBrowser && !storageListeners.size) { + off(window, 'storage', storageEventHandler); + } }; export type UseStorageValueOptions = { - /** - * Value to return if `key` is not present in LocalStorage. - * - * @default undefined - */ - defaultValue?: T; - - /** - * Fetch storage value on first render. If set to `false` will make the hook yield `undefined` on - * first render and defer fetching of the value until effects are executed. - * - * @default true - */ - initializeWithValue?: InitializeWithValue; - - /** - * Custom function to parse storage value with. - */ - parse?: (str: string | null, fallback: T | null) => T | null; - - /** - * Custom function to stringify value to store with. - */ - stringify?: (data: T) => string | null; + /** + * Value to return if `key` is not present in LocalStorage. + * + * @default undefined + */ + defaultValue?: T; + + /** + * Fetch storage value on first render. If set to `false` will make the hook yield `undefined` on + * first render and defer fetching of the value until effects are executed. + * + * @default true + */ + initializeWithValue?: InitializeWithValue; + + /** + * Custom function to parse storage value with. + */ + parse?: (str: string | null, fallback: T | null) => T | null; + + /** + * Custom function to stringify value to store with. + */ + stringify?: (data: T) => string | null; }; type UseStorageValueValue< - Type, - Default extends Type = Type, - Initialize extends boolean | undefined = boolean | undefined, - N = Default extends null | undefined ? null | Type : Type, - U = Initialize extends false ? undefined | N : N + Type, + Default extends Type = Type, + Initialize extends boolean | undefined = boolean | undefined, + N = Default extends null | undefined ? null | Type : Type, + U = Initialize extends false ? undefined | N : N > = U; export type UseStorageValueResult< - Type, - Default extends Type = Type, - Initialize extends boolean | undefined = boolean | undefined + Type, + Default extends Type = Type, + Initialize extends boolean | undefined = boolean | undefined > = { - value: UseStorageValueValue; + value: UseStorageValueValue; - set: (val: NextState>) => void; - remove: () => void; - fetch: () => void; + set: (val: NextState>) => void; + remove: () => void; + fetch: () => void; }; const DEFAULT_OPTIONS = { - defaultValue: null, - initializeWithValue: true, + defaultValue: null, + initializeWithValue: true, }; export function useStorageValue< - Type, - Default extends Type = Type, - Initialize extends boolean | undefined = boolean | undefined + Type, + Default extends Type = Type, + Initialize extends boolean | undefined = boolean | undefined >( - storage: Storage, - key: string, - options?: UseStorageValueOptions + storage: Storage, + key: string, + options?: UseStorageValueOptions ): UseStorageValueResult { - const optionsRef = useSyncedRef({ ...DEFAULT_OPTIONS, ...options }); - const parse = (str: string | null, fallback: Type | null): Type | null => { - const parseFunction = optionsRef.current.parse ?? defaultParse; - return parseFunction(str, fallback); - }; - - const stringify = (data: Type): string | null => { - const stringifyFunction = optionsRef.current.stringify ?? defaultStringify; - return stringifyFunction(data); - }; - - const storageActions = useSyncedRef({ - fetchRaw: () => storage.getItem(key), - fetch: () => - parse( - storageActions.current.fetchRaw(), - optionsRef.current.defaultValue as Required | null - ), - remove() { - storage.removeItem(key); - }, - store(val: Type): string | null { - const stringified = stringify(val); - - if (stringified !== null) { - storage.setItem(key, stringified); - } - - return stringified; - }, - }); - - const isFirstMount = useFirstMountState(); - const [state, setState] = useState( - optionsRef.current?.initializeWithValue && isFirstMount - ? storageActions.current.fetch() - : undefined - ); - const stateRef = useSyncedRef(state); - - const stateActions = useSyncedRef({ - fetch() { - setState(storageActions.current.fetch()); - }, - setRawVal(val: string | null) { - setState(parse(val, optionsRef.current.defaultValue)); - }, - }); - - useUpdateEffect(() => { - stateActions.current.fetch(); - }, [key]); - - useEffect(() => { - if (!optionsRef.current.initializeWithValue) { - stateActions.current.fetch(); - } - }, []); - - useIsomorphicLayoutEffect(() => { - const handler = stateActions.current.setRawVal; - - addStorageListener(storage, key, handler); - - return () => { - removeStorageListener(storage, key, handler); - }; - }, [storage, key]); - - const actions = useSyncedRef({ - set(val: NextState>) { - if (!isBrowser) return; - - const s = resolveHookState( - val, - stateRef.current as UseStorageValueValue - ); - - const storeVal = storageActions.current.store(s); - if (storeVal !== null) { - invokeStorageKeyListeners(storage, key, storeVal); - } - }, - delete() { - if (!isBrowser) return; - - storageActions.current.remove(); - invokeStorageKeyListeners(storage, key, null); - }, - fetch() { - if (!isBrowser) return; - - invokeStorageKeyListeners(storage, key, storageActions.current.fetchRaw()); - }, - }); - - // Make actions static so developers can pass methods further - const staticActions = useMemo( - () => ({ - set: ((v) => { - actions.current.set(v); - }) as typeof actions.current.set, - remove() { - actions.current.delete(); - }, - fetch() { - actions.current.fetch(); - }, - }), - - [] - ); - - return useMemo( - () => ({ - value: state as UseStorageValueValue, - ...staticActions, - }), - - [state] - ); + const optionsRef = useSyncedRef({ ...DEFAULT_OPTIONS, ...options }); + const parse = (str: string | null, fallback: Type | null): Type | null => { + const parseFunction = optionsRef.current.parse ?? defaultParse; + return parseFunction(str, fallback); + }; + + const stringify = (data: Type): string | null => { + const stringifyFunction = optionsRef.current.stringify ?? defaultStringify; + return stringifyFunction(data); + }; + + const storageActions = useSyncedRef({ + fetchRaw: () => storage.getItem(key), + fetch: () => + parse( + storageActions.current.fetchRaw(), + optionsRef.current.defaultValue as Required | null + ), + remove() { + storage.removeItem(key); + }, + store(val: Type): string | null { + const stringified = stringify(val); + + if (stringified !== null) { + storage.setItem(key, stringified); + } + + return stringified; + }, + }); + + const isFirstMount = useFirstMountState(); + const [state, setState] = useState( + optionsRef.current?.initializeWithValue && isFirstMount + ? storageActions.current.fetch() + : undefined + ); + const stateRef = useSyncedRef(state); + + const stateActions = useSyncedRef({ + fetch() { + setState(storageActions.current.fetch()); + }, + setRawVal(val: string | null) { + setState(parse(val, optionsRef.current.defaultValue)); + }, + }); + + useUpdateEffect(() => { + stateActions.current.fetch(); + }, [key]); + + useEffect(() => { + if (!optionsRef.current.initializeWithValue) { + stateActions.current.fetch(); + } + }, []); + + useIsomorphicLayoutEffect(() => { + const handler = stateActions.current.setRawVal; + + addStorageListener(storage, key, handler); + + return () => { + removeStorageListener(storage, key, handler); + }; + }, [storage, key]); + + const actions = useSyncedRef({ + set(val: NextState>) { + if (!isBrowser) return; + + const s = resolveHookState( + val, + stateRef.current as UseStorageValueValue + ); + + const storeVal = storageActions.current.store(s); + if (storeVal !== null) { + invokeStorageKeyListeners(storage, key, storeVal); + } + }, + delete() { + if (!isBrowser) return; + + storageActions.current.remove(); + invokeStorageKeyListeners(storage, key, null); + }, + fetch() { + if (!isBrowser) return; + + invokeStorageKeyListeners(storage, key, storageActions.current.fetchRaw()); + }, + }); + + // Make actions static so developers can pass methods further + const staticActions = useMemo( + () => ({ + set: ((v) => { + actions.current.set(v); + }) as typeof actions.current.set, + remove() { + actions.current.delete(); + }, + fetch() { + actions.current.fetch(); + }, + }), + + [] + ); + + return useMemo( + () => ({ + value: state as UseStorageValueValue, + ...staticActions, + }), + + [state] + ); } const defaultStringify = (data: unknown): string | null => { - if (data === null) { - /* istanbul ignore next */ - if (process.env.NODE_ENV === 'development') { - console.warn( - `'null' is not a valid data for useStorageValue hook, this operation will take no effect` - ); - } - - return null; - } - - try { - return JSON.stringify(data); - } catch (error) /* istanbul ignore next */ { - // I have absolutely no idea how to cover this, since modern JSON.stringify does not throw on - // cyclic references anymore - - console.warn(error); - return null; - } + if (data === null) { + /* istanbul ignore next */ + if (process.env.NODE_ENV === 'development') { + console.warn( + `'null' is not a valid data for useStorageValue hook, this operation will take no effect` + ); + } + + return null; + } + + try { + return JSON.stringify(data); + } catch (error) /* istanbul ignore next */ { + // I have absolutely no idea how to cover this, since modern JSON.stringify does not throw on + // cyclic references anymore + + console.warn(error); + return null; + } }; const defaultParse = (str: string | null, fallback: T | null): T | null => { - if (str === null) return fallback; + if (str === null) return fallback; - try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return JSON.parse(str); - } catch (error) { - console.warn(error); + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return JSON.parse(str); + } catch (error) { + console.warn(error); - return fallback; - } + return fallback; + } }; diff --git a/src/useSyncedRef/__docs__/example.stories.tsx b/src/useSyncedRef/__docs__/example.stories.tsx index 09fa92844..eab6dde15 100644 --- a/src/useSyncedRef/__docs__/example.stories.tsx +++ b/src/useSyncedRef/__docs__/example.stories.tsx @@ -2,17 +2,17 @@ import React, { useRef } from 'react'; import { useRerender, useSyncedRef } from '../..'; export function Example() { - const ref = useRef(0); - const syncedRef = useSyncedRef(++ref.current); - const rerender = useRerender(); + const ref = useRef(0); + const syncedRef = useSyncedRef(++ref.current); + const rerender = useRerender(); - return ( -
-
As you may see in source code, ref value updated automatically
- {' '} - renders: {syncedRef.current} -
- ); + return ( +
+
As you may see in source code, ref value updated automatically
+ {' '} + renders: {syncedRef.current} +
+ ); } diff --git a/src/useSyncedRef/__docs__/story.mdx b/src/useSyncedRef/__docs__/story.mdx index cdfd6e565..1996f5216 100644 --- a/src/useSyncedRef/__docs__/story.mdx +++ b/src/useSyncedRef/__docs__/story.mdx @@ -25,7 +25,7 @@ Like `useRef`, but it returns immutable ref that contains actual value. #### Example - + ## Reference diff --git a/src/useSyncedRef/__tests__/dom.ts b/src/useSyncedRef/__tests__/dom.ts index 760b53c15..f8ce7e97f 100644 --- a/src/useSyncedRef/__tests__/dom.ts +++ b/src/useSyncedRef/__tests__/dom.ts @@ -2,58 +2,58 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useSyncedRef } from '../..'; describe('useSyncedRef', () => { - it('should be defined', () => { - expect(useSyncedRef).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useSyncedRef(1)); - expect(result.error).toBeUndefined(); - }); - - it('should return ref object', () => { - const { result } = renderHook(() => useSyncedRef(1)); - - expect(result.current).toEqual({ current: 1 }); - }); - - it('should return same ref between renders', () => { - const { result, rerender } = renderHook(() => useSyncedRef(1)); - - const ref = result.current; - rerender(); - expect(result.current).toEqual(ref); - rerender(); - expect(result.current).toEqual(ref); - rerender(); - expect(result.current).toEqual(ref); - }); - - it('should contain actual value on each render', () => { - const { result, rerender } = renderHook(({ val }) => useSyncedRef(val), { - initialProps: { val: 1 as any }, - }); - - expect(result.current.current).toBe(1); - const value1 = { foo: 'bar' }; - rerender({ val: value1 }); - expect(result.current.current).toBe(value1); - const value2 = ['a', 'b', 'c']; - rerender({ val: value2 }); - expect(result.current.current).toBe(value2); - }); - - it('should throw on attempt to change ref', () => { - const { result } = renderHook(() => useSyncedRef(1)); - - expect(() => { - // @ts-expect-error testing irrelevant usage - result.current.foo = 'bar'; - }).toThrow(new TypeError('Cannot add property foo, object is not extensible')); - - expect(() => { - // @ts-expect-error testing irrelevant usage - result.current.current = 2; - }).toThrow(new TypeError('Cannot set property current of # which has only a getter')); - }); + it('should be defined', () => { + expect(useSyncedRef).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useSyncedRef(1)); + expect(result.error).toBeUndefined(); + }); + + it('should return ref object', () => { + const { result } = renderHook(() => useSyncedRef(1)); + + expect(result.current).toEqual({ current: 1 }); + }); + + it('should return same ref between renders', () => { + const { result, rerender } = renderHook(() => useSyncedRef(1)); + + const ref = result.current; + rerender(); + expect(result.current).toEqual(ref); + rerender(); + expect(result.current).toEqual(ref); + rerender(); + expect(result.current).toEqual(ref); + }); + + it('should contain actual value on each render', () => { + const { result, rerender } = renderHook(({ val }) => useSyncedRef(val), { + initialProps: { val: 1 as any }, + }); + + expect(result.current.current).toBe(1); + const value1 = { foo: 'bar' }; + rerender({ val: value1 }); + expect(result.current.current).toBe(value1); + const value2 = ['a', 'b', 'c']; + rerender({ val: value2 }); + expect(result.current.current).toBe(value2); + }); + + it('should throw on attempt to change ref', () => { + const { result } = renderHook(() => useSyncedRef(1)); + + expect(() => { + // @ts-expect-error testing irrelevant usage + result.current.foo = 'bar'; + }).toThrow(new TypeError('Cannot add property foo, object is not extensible')); + + expect(() => { + // @ts-expect-error testing irrelevant usage + result.current.current = 2; + }).toThrow(new TypeError('Cannot set property current of # which has only a getter')); + }); }); diff --git a/src/useSyncedRef/__tests__/ssr.ts b/src/useSyncedRef/__tests__/ssr.ts index 334a5f620..af543cf59 100644 --- a/src/useSyncedRef/__tests__/ssr.ts +++ b/src/useSyncedRef/__tests__/ssr.ts @@ -2,18 +2,18 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useSyncedRef } from '../..'; describe('useSyncedRef', () => { - it('should be defined', () => { - expect(useSyncedRef).toBeDefined(); - }); + it('should be defined', () => { + expect(useSyncedRef).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useSyncedRef(1)); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useSyncedRef(1)); + expect(result.error).toBeUndefined(); + }); - it('should return ref object', () => { - const { result } = renderHook(() => useSyncedRef(1)); + it('should return ref object', () => { + const { result } = renderHook(() => useSyncedRef(1)); - expect(result.current).toEqual({ current: 1 }); - }); + expect(result.current).toEqual({ current: 1 }); + }); }); diff --git a/src/useSyncedRef/index.ts b/src/useSyncedRef/index.ts index 785621fc3..8581564a4 100644 --- a/src/useSyncedRef/index.ts +++ b/src/useSyncedRef/index.ts @@ -6,17 +6,17 @@ import { useMemo, useRef } from 'react'; * @param value */ export function useSyncedRef(value: T): { readonly current: T } { - const ref = useRef(value); + const ref = useRef(value); - ref.current = value; + ref.current = value; - return useMemo( - () => - Object.freeze({ - get current() { - return ref.current; - }, - }), - [] - ); + return useMemo( + () => + Object.freeze({ + get current() { + return ref.current; + }, + }), + [] + ); } diff --git a/src/useThrottledCallback/__docs__/example.stories.tsx b/src/useThrottledCallback/__docs__/example.stories.tsx index cdabb5b0b..96729bae3 100644 --- a/src/useThrottledCallback/__docs__/example.stories.tsx +++ b/src/useThrottledCallback/__docs__/example.stories.tsx @@ -2,24 +2,24 @@ import React, { type ComponentProps, useState } from 'react'; import { useThrottledCallback } from '../..'; export function Example() { - const [state, setState] = useState(''); + const [state, setState] = useState(''); - const handleChange: React.ChangeEventHandler = useThrottledCallback< - NonNullable['onChange']> - >( - (ev) => { - setState(ev.target.value); - }, - [], - 500 - ); + const handleChange: React.ChangeEventHandler = useThrottledCallback< + NonNullable['onChange']> + >( + (ev) => { + setState(ev.target.value); + }, + [], + 500 + ); - return ( -
-
Below state will update no more than once every 500ms
-
-
The input`s value is: {state}
- -
- ); + return ( +
+
Below state will update no more than once every 500ms
+
+
The input`s value is: {state}
+ +
+ ); } diff --git a/src/useThrottledCallback/__docs__/story.mdx b/src/useThrottledCallback/__docs__/story.mdx index 317231d33..bc27de401 100644 --- a/src/useThrottledCallback/__docs__/story.mdx +++ b/src/useThrottledCallback/__docs__/story.mdx @@ -18,17 +18,17 @@ Throttled function is always a void function since original callback invoked lat #### Example - + ## Reference ```ts export function useThrottledCallback( - callback: (this: This, ...args: Args) => any, - deps: DependencyList, - delay: number, - noTrailing = false + callback: (this: This, ...args: Args) => any, + deps: DependencyList, + delay: number, + noTrailing = false ): ThrottledFunction; ``` diff --git a/src/useThrottledCallback/__tests__/dom.ts b/src/useThrottledCallback/__tests__/dom.ts index a6664b6fd..d09f73563 100644 --- a/src/useThrottledCallback/__tests__/dom.ts +++ b/src/useThrottledCallback/__tests__/dom.ts @@ -2,114 +2,114 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useThrottledCallback } from '../..'; describe('useThrottledCallback', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.clearAllTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - it('should be defined', () => { - expect(useThrottledCallback).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => { - useThrottledCallback(() => {}, [], 200); - }); - expect(result.error).toBeUndefined(); - }); - - it('should return function same length and wrapped name', () => { - let { result } = renderHook(() => - useThrottledCallback((_a: any, _b: any, _c: any) => {}, [], 200) - ); - - expect(result.current.length).toBe(3); - expect(result.current.name).toBe(`anonymous__throttled__200`); - - function testFn(_a: any, _b: any, _c: any) {} - - result = renderHook(() => useThrottledCallback(testFn, [], 100)).result; - - expect(result.current.length).toBe(3); - expect(result.current.name).toBe(`testFn__throttled__100`); - }); - - it('should return new callback if delay is changed', () => { - const { result, rerender } = renderHook( - ({ delay }) => useThrottledCallback(() => {}, [], delay), - { - initialProps: { delay: 200 }, - } - ); - - const cb1 = result.current; - rerender({ delay: 123 }); - - expect(cb1).not.toBe(result.current); - }); - - it('should invoke given callback immediately', () => { - const cb = jest.fn(); - const { result } = renderHook(() => useThrottledCallback(cb, [], 200)); - - result.current(); - expect(cb).toHaveBeenCalledTimes(1); - }); - - it('should pass parameters to callback', () => { - const cb = jest.fn((_a: number, _c: string) => {}); - const { result } = renderHook(() => useThrottledCallback(cb, [], 200)); - - result.current(1, 'abc'); - expect(cb).toHaveBeenCalledWith(1, 'abc'); - }); - - it('should ignore consequential calls occurred within delay, but execute last call after delay is passed', () => { - const cb = jest.fn(); - const { result } = renderHook(() => useThrottledCallback(cb, [], 200)); - - result.current(); - result.current(); - result.current(); - result.current(); - expect(cb).toHaveBeenCalledTimes(1); - jest.advanceTimersByTime(199); - result.current(); - expect(cb).toHaveBeenCalledTimes(1); - jest.advanceTimersByTime(1); - expect(cb).toHaveBeenCalledTimes(2); - result.current(); - expect(cb).toHaveBeenCalledTimes(2); - jest.advanceTimersByTime(200); - expect(cb).toHaveBeenCalledTimes(3); - }); - - it('should drop trailing execution if `noTrailing is set to true`', () => { - const cb = jest.fn(); - const { result } = renderHook(() => useThrottledCallback(cb, [], 200, true)); - - result.current(); - result.current(); - result.current(); - result.current(); - expect(cb).toHaveBeenCalledTimes(1); - jest.advanceTimersByTime(199); - result.current(); - expect(cb).toHaveBeenCalledTimes(1); - jest.advanceTimersByTime(1); - expect(cb).toHaveBeenCalledTimes(1); - result.current(); - result.current(); - result.current(); - expect(cb).toHaveBeenCalledTimes(2); - jest.advanceTimersByTime(200); - expect(cb).toHaveBeenCalledTimes(2); - }); + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should be defined', () => { + expect(useThrottledCallback).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => { + useThrottledCallback(() => {}, [], 200); + }); + expect(result.error).toBeUndefined(); + }); + + it('should return function same length and wrapped name', () => { + let { result } = renderHook(() => + useThrottledCallback((_a: any, _b: any, _c: any) => {}, [], 200) + ); + + expect(result.current.length).toBe(3); + expect(result.current.name).toBe(`anonymous__throttled__200`); + + function testFn(_a: any, _b: any, _c: any) {} + + result = renderHook(() => useThrottledCallback(testFn, [], 100)).result; + + expect(result.current.length).toBe(3); + expect(result.current.name).toBe(`testFn__throttled__100`); + }); + + it('should return new callback if delay is changed', () => { + const { result, rerender } = renderHook( + ({ delay }) => useThrottledCallback(() => {}, [], delay), + { + initialProps: { delay: 200 }, + } + ); + + const cb1 = result.current; + rerender({ delay: 123 }); + + expect(cb1).not.toBe(result.current); + }); + + it('should invoke given callback immediately', () => { + const cb = jest.fn(); + const { result } = renderHook(() => useThrottledCallback(cb, [], 200)); + + result.current(); + expect(cb).toHaveBeenCalledTimes(1); + }); + + it('should pass parameters to callback', () => { + const cb = jest.fn((_a: number, _c: string) => {}); + const { result } = renderHook(() => useThrottledCallback(cb, [], 200)); + + result.current(1, 'abc'); + expect(cb).toHaveBeenCalledWith(1, 'abc'); + }); + + it('should ignore consequential calls occurred within delay, but execute last call after delay is passed', () => { + const cb = jest.fn(); + const { result } = renderHook(() => useThrottledCallback(cb, [], 200)); + + result.current(); + result.current(); + result.current(); + result.current(); + expect(cb).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(199); + result.current(); + expect(cb).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(1); + expect(cb).toHaveBeenCalledTimes(2); + result.current(); + expect(cb).toHaveBeenCalledTimes(2); + jest.advanceTimersByTime(200); + expect(cb).toHaveBeenCalledTimes(3); + }); + + it('should drop trailing execution if `noTrailing is set to true`', () => { + const cb = jest.fn(); + const { result } = renderHook(() => useThrottledCallback(cb, [], 200, true)); + + result.current(); + result.current(); + result.current(); + result.current(); + expect(cb).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(199); + result.current(); + expect(cb).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(1); + expect(cb).toHaveBeenCalledTimes(1); + result.current(); + result.current(); + result.current(); + expect(cb).toHaveBeenCalledTimes(2); + jest.advanceTimersByTime(200); + expect(cb).toHaveBeenCalledTimes(2); + }); }); diff --git a/src/useThrottledCallback/__tests__/ssr.ts b/src/useThrottledCallback/__tests__/ssr.ts index 926205f90..c843628e3 100644 --- a/src/useThrottledCallback/__tests__/ssr.ts +++ b/src/useThrottledCallback/__tests__/ssr.ts @@ -2,43 +2,43 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useThrottledCallback } from '../..'; describe('useThrottledCallback', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.clearAllTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - it('should be defined', () => { - expect(useThrottledCallback).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => { - useThrottledCallback(() => {}, [], 200); - }); - expect(result.error).toBeUndefined(); - }); - - it('should invoke given callback immediately', () => { - const cb = jest.fn(); - const { result } = renderHook(() => useThrottledCallback(cb, [], 200)); - - result.current(); - expect(cb).toHaveBeenCalledTimes(1); - }); - - it('should pass parameters to callback', () => { - const cb = jest.fn((_a: number, _c: string) => {}); - const { result } = renderHook(() => useThrottledCallback(cb, [], 200)); - - result.current(1, 'abc'); - jest.advanceTimersByTime(200); - expect(cb).toHaveBeenCalledWith(1, 'abc'); - }); + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should be defined', () => { + expect(useThrottledCallback).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => { + useThrottledCallback(() => {}, [], 200); + }); + expect(result.error).toBeUndefined(); + }); + + it('should invoke given callback immediately', () => { + const cb = jest.fn(); + const { result } = renderHook(() => useThrottledCallback(cb, [], 200)); + + result.current(); + expect(cb).toHaveBeenCalledTimes(1); + }); + + it('should pass parameters to callback', () => { + const cb = jest.fn((_a: number, _c: string) => {}); + const { result } = renderHook(() => useThrottledCallback(cb, [], 200)); + + result.current(1, 'abc'); + jest.advanceTimersByTime(200); + expect(cb).toHaveBeenCalledWith(1, 'abc'); + }); }); diff --git a/src/useThrottledCallback/index.ts b/src/useThrottledCallback/index.ts index 887b9dc7c..a067d717e 100644 --- a/src/useThrottledCallback/index.ts +++ b/src/useThrottledCallback/index.ts @@ -2,8 +2,8 @@ import { type DependencyList, useMemo, useRef } from 'react'; import { useUnmountEffect } from '../useUnmountEffect'; export type ThrottledFunction any> = ( - this: ThisParameterType, - ...args: Parameters + this: ThisParameterType, + ...args: Parameters ) => void; /** @@ -17,56 +17,56 @@ export type ThrottledFunction any> = ( * after the last throttled-function call. */ export function useThrottledCallback any>( - callback: Fn, - deps: DependencyList, - delay: number, - noTrailing = false + callback: Fn, + deps: DependencyList, + delay: number, + noTrailing = false ): ThrottledFunction { - const timeout = useRef>(); - const lastCall = useRef<{ args: Parameters; this: ThisParameterType }>(); + const timeout = useRef>(); + const lastCall = useRef<{ args: Parameters; this: ThisParameterType }>(); - useUnmountEffect(() => { - if (timeout.current) { - clearTimeout(timeout.current); - timeout.current = undefined; - } - }); + useUnmountEffect(() => { + if (timeout.current) { + clearTimeout(timeout.current); + timeout.current = undefined; + } + }); - return useMemo(() => { - const execute = (context: ThisParameterType, args: Parameters) => { - lastCall.current = undefined; - callback.apply(context, args); + return useMemo(() => { + const execute = (context: ThisParameterType, args: Parameters) => { + lastCall.current = undefined; + callback.apply(context, args); - timeout.current = setTimeout(() => { - timeout.current = undefined; + timeout.current = setTimeout(() => { + timeout.current = undefined; - // If trailing execution is not disabled - call callback with last - // received arguments and context - if (!noTrailing && lastCall.current) { - execute(lastCall.current.this, lastCall.current.args); + // If trailing execution is not disabled - call callback with last + // received arguments and context + if (!noTrailing && lastCall.current) { + execute(lastCall.current.this, lastCall.current.args); - lastCall.current = undefined; - } - }, delay); - }; + lastCall.current = undefined; + } + }, delay); + }; - const wrapped = function (this, ...args) { - if (timeout.current) { - // If we cant execute callback immediately - save its arguments and - // context to execute it when delay is passed - lastCall.current = { args, this: this }; + const wrapped = function (this, ...args) { + if (timeout.current) { + // If we cant execute callback immediately - save its arguments and + // context to execute it when delay is passed + lastCall.current = { args, this: this }; - return; - } + return; + } - execute(this, args); - } as ThrottledFunction; + execute(this, args); + } as ThrottledFunction; - Object.defineProperties(wrapped, { - length: { value: callback.length }, - name: { value: `${callback.name || 'anonymous'}__throttled__${delay}` }, - }); + Object.defineProperties(wrapped, { + length: { value: callback.length }, + name: { value: `${callback.name || 'anonymous'}__throttled__${delay}` }, + }); - return wrapped; - }, [delay, noTrailing, ...deps]); + return wrapped; + }, [delay, noTrailing, ...deps]); } diff --git a/src/useThrottledEffect/__docs__/example.stories.tsx b/src/useThrottledEffect/__docs__/example.stories.tsx index 0876e03ba..9286ae0e1 100644 --- a/src/useThrottledEffect/__docs__/example.stories.tsx +++ b/src/useThrottledEffect/__docs__/example.stories.tsx @@ -5,29 +5,29 @@ import { useThrottledEffect } from '../..'; const HAS_DIGIT_REGEX = /\d/g; export function Example() { - const [state, setState] = useState(''); - const [hasNumbers, setHasNumbers] = useState(false); + const [state, setState] = useState(''); + const [hasNumbers, setHasNumbers] = useState(false); - useThrottledEffect( - () => { - setHasNumbers(HAS_DIGIT_REGEX.test(state)); - }, - [state], - 200 - ); + useThrottledEffect( + () => { + setHasNumbers(HAS_DIGIT_REGEX.test(state)); + }, + [state], + 200 + ); - return ( -
-
Digit check will be performed no more than once every 200ms
-
-
{hasNumbers ? 'Input has digits' : 'No digits found in input'}
- { - setState(ev.target.value); - }} - /> -
- ); + return ( +
+
Digit check will be performed no more than once every 200ms
+
+
{hasNumbers ? 'Input has digits' : 'No digits found in input'}
+ { + setState(ev.target.value); + }} + /> +
+ ); } diff --git a/src/useThrottledEffect/__docs__/story.mdx b/src/useThrottledEffect/__docs__/story.mdx index 4f1cd4967..41793c425 100644 --- a/src/useThrottledEffect/__docs__/story.mdx +++ b/src/useThrottledEffect/__docs__/story.mdx @@ -11,17 +11,17 @@ Like `useEffect`, but passed function is throttled. #### Example - + ## Reference ```ts export function useThrottledEffect( - callback: (...args: any[]) => void, - deps: DependencyList, - delay: number, - noTrailing = false + callback: (...args: any[]) => void, + deps: DependencyList, + delay: number, + noTrailing = false ): void; ``` diff --git a/src/useThrottledEffect/__tests__/dom.ts b/src/useThrottledEffect/__tests__/dom.ts index 4e42b3b4a..51214345e 100644 --- a/src/useThrottledEffect/__tests__/dom.ts +++ b/src/useThrottledEffect/__tests__/dom.ts @@ -2,49 +2,49 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useThrottledEffect } from '../..'; describe('useThrottledEffect', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.clearAllTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - it('should be defined', () => { - expect(useThrottledEffect).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => { - useThrottledEffect(() => {}, [], 200); - }); - expect(result.error).toBeUndefined(); - }); - - it('should throttle passed callback', () => { - const spy = jest.fn(); - const { rerender } = renderHook( - (dep) => { - useThrottledEffect(spy, [dep], 200, true); - }, - { - initialProps: 1, - } - ); - - expect(spy).toHaveBeenCalledTimes(1); - rerender(2); - rerender(3); - rerender(4); - expect(spy).toHaveBeenCalledTimes(1); - - jest.advanceTimersByTime(200); - expect(spy).toHaveBeenCalledTimes(1); - rerender(5); - expect(spy).toHaveBeenCalledTimes(2); - }); + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should be defined', () => { + expect(useThrottledEffect).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => { + useThrottledEffect(() => {}, [], 200); + }); + expect(result.error).toBeUndefined(); + }); + + it('should throttle passed callback', () => { + const spy = jest.fn(); + const { rerender } = renderHook( + (dep) => { + useThrottledEffect(spy, [dep], 200, true); + }, + { + initialProps: 1, + } + ); + + expect(spy).toHaveBeenCalledTimes(1); + rerender(2); + rerender(3); + rerender(4); + expect(spy).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(200); + expect(spy).toHaveBeenCalledTimes(1); + rerender(5); + expect(spy).toHaveBeenCalledTimes(2); + }); }); diff --git a/src/useThrottledEffect/__tests__/ssr.ts b/src/useThrottledEffect/__tests__/ssr.ts index 3dd92bbb3..ed1829ca9 100644 --- a/src/useThrottledEffect/__tests__/ssr.ts +++ b/src/useThrottledEffect/__tests__/ssr.ts @@ -2,26 +2,26 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useThrottledEffect } from '../..'; describe('useThrottledEffect', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); + beforeAll(() => { + jest.useFakeTimers(); + }); - afterEach(() => { - jest.clearAllTimers(); - }); + afterEach(() => { + jest.clearAllTimers(); + }); - afterAll(() => { - jest.useRealTimers(); - }); + afterAll(() => { + jest.useRealTimers(); + }); - it('should be defined', () => { - expect(useThrottledEffect).toBeDefined(); - }); + it('should be defined', () => { + expect(useThrottledEffect).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useThrottledEffect(() => {}, [], 200); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useThrottledEffect(() => {}, [], 200); + }); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useThrottledEffect/index.ts b/src/useThrottledEffect/index.ts index 00fe9df9c..b522a8e87 100644 --- a/src/useThrottledEffect/index.ts +++ b/src/useThrottledEffect/index.ts @@ -14,10 +14,10 @@ import { useThrottledCallback } from '../useThrottledCallback'; * after the last throttled-function call. */ export function useThrottledEffect( - callback: (...args: any[]) => void, - deps: DependencyList, - delay: number, - noTrailing = false + callback: (...args: any[]) => void, + deps: DependencyList, + delay: number, + noTrailing = false ): void { - useEffect(useThrottledCallback(callback, deps, delay, noTrailing), deps); + useEffect(useThrottledCallback(callback, deps, delay, noTrailing), deps); } diff --git a/src/useThrottledState/__docs__/example.stories.tsx b/src/useThrottledState/__docs__/example.stories.tsx index e926b5929..fb68b9e0f 100644 --- a/src/useThrottledState/__docs__/example.stories.tsx +++ b/src/useThrottledState/__docs__/example.stories.tsx @@ -2,19 +2,19 @@ import React from 'react'; import { useThrottledState } from '../..'; export function Example() { - const [state, setState] = useThrottledState('', 500); + const [state, setState] = useThrottledState('', 500); - return ( -
-
Below state will update no more than once every 500ms
-
-
The input`s value is: {state}
- { - setState(ev.target.value); - }} - /> -
- ); + return ( +
+
Below state will update no more than once every 500ms
+
+
The input`s value is: {state}
+ { + setState(ev.target.value); + }} + /> +
+ ); } diff --git a/src/useThrottledState/__docs__/story.mdx b/src/useThrottledState/__docs__/story.mdx index 5e66c216f..648bf1a53 100644 --- a/src/useThrottledState/__docs__/story.mdx +++ b/src/useThrottledState/__docs__/story.mdx @@ -11,16 +11,16 @@ Like `useState` but its state setter is throttled. #### Example - + ## Reference ```ts export function useThrottledState( - initialState: S | (() => S), - delay: number, - noTrailing = false + initialState: S | (() => S), + delay: number, + noTrailing = false ): [S, Dispatch>]; ``` diff --git a/src/useThrottledState/__tests__/dom.ts b/src/useThrottledState/__tests__/dom.ts index 77d5c1f76..dc0a1d833 100644 --- a/src/useThrottledState/__tests__/dom.ts +++ b/src/useThrottledState/__tests__/dom.ts @@ -2,43 +2,43 @@ import { renderHook, act } from '@testing-library/react-hooks/dom'; import { useThrottledState } from '../..'; describe('useThrottledState', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.clearAllTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - it('should be defined', () => { - expect(useThrottledState).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useThrottledState('', 200)); - expect(result.error).toBeUndefined(); - }); - - it('should throttle set state', () => { - const { result } = renderHook(() => useThrottledState('', 200, true)); - - expect(result.current[0]).toBe(''); - act(() => { - result.current[1]('hello world!'); - }); - expect(result.current[0]).toBe('hello world!'); - - result.current[1]('foo'); - result.current[1]('bar'); - expect(result.current[0]).toBe('hello world!'); - jest.advanceTimersByTime(200); - act(() => { - result.current[1]('baz'); - }); - expect(result.current[0]).toBe('baz'); - }); + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should be defined', () => { + expect(useThrottledState).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useThrottledState('', 200)); + expect(result.error).toBeUndefined(); + }); + + it('should throttle set state', () => { + const { result } = renderHook(() => useThrottledState('', 200, true)); + + expect(result.current[0]).toBe(''); + act(() => { + result.current[1]('hello world!'); + }); + expect(result.current[0]).toBe('hello world!'); + + result.current[1]('foo'); + result.current[1]('bar'); + expect(result.current[0]).toBe('hello world!'); + jest.advanceTimersByTime(200); + act(() => { + result.current[1]('baz'); + }); + expect(result.current[0]).toBe('baz'); + }); }); diff --git a/src/useThrottledState/__tests__/ssr.ts b/src/useThrottledState/__tests__/ssr.ts index 691583154..ac36f2113 100644 --- a/src/useThrottledState/__tests__/ssr.ts +++ b/src/useThrottledState/__tests__/ssr.ts @@ -2,24 +2,24 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useThrottledState } from '../..'; describe('useThrottledState', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); + beforeAll(() => { + jest.useFakeTimers(); + }); - afterEach(() => { - jest.clearAllTimers(); - }); + afterEach(() => { + jest.clearAllTimers(); + }); - afterAll(() => { - jest.useRealTimers(); - }); + afterAll(() => { + jest.useRealTimers(); + }); - it('should be defined', () => { - expect(useThrottledState).toBeDefined(); - }); + it('should be defined', () => { + expect(useThrottledState).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useThrottledState('', 200)); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useThrottledState('', 200)); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useThrottledState/index.ts b/src/useThrottledState/index.ts index c66268ebc..53b205b33 100644 --- a/src/useThrottledState/index.ts +++ b/src/useThrottledState/index.ts @@ -11,11 +11,11 @@ import { useThrottledCallback } from '../useThrottledCallback'; * after the last throttled-function call. */ export function useThrottledState( - initialState: S | (() => S), - delay: number, - noTrailing = false + initialState: S | (() => S), + delay: number, + noTrailing = false ): [S, Dispatch>] { - const [state, setState] = useState(initialState); + const [state, setState] = useState(initialState); - return [state, useThrottledCallback(setState, [], delay, noTrailing)]; + return [state, useThrottledCallback(setState, [], delay, noTrailing)]; } diff --git a/src/useTimeoutEffect/__docs__/example.stories.tsx b/src/useTimeoutEffect/__docs__/example.stories.tsx index 267a5344d..acfed65e4 100644 --- a/src/useTimeoutEffect/__docs__/example.stories.tsx +++ b/src/useTimeoutEffect/__docs__/example.stories.tsx @@ -3,68 +3,68 @@ import { useState } from 'react'; import { useTimeoutEffect, useToggle } from '../..'; export function Example() { - const [numCalls, setNumCalls] = useState(0); - const [enabled, toggleEnabled] = useToggle(); - const [timeoutValue, setTimeoutValue] = useState(1000); - const [cancelled, toggleCancelled] = useToggle(); + const [numCalls, setNumCalls] = useState(0); + const [enabled, toggleEnabled] = useToggle(); + const [timeoutValue, setTimeoutValue] = useState(1000); + const [cancelled, toggleCancelled] = useToggle(); - let status; - if (cancelled) { - status = 'Cancelled'; - } else { - status = enabled ? 'Enabled' : 'Disabled'; - } + let status; + if (cancelled) { + status = 'Cancelled'; + } else { + status = enabled ? 'Enabled' : 'Disabled'; + } - const [cancel, reset] = useTimeoutEffect( - () => { - setNumCalls((n) => n + 1); - }, - enabled ? timeoutValue : undefined - ); + const [cancel, reset] = useTimeoutEffect( + () => { + setNumCalls((n) => n + 1); + }, + enabled ? timeoutValue : undefined + ); - React.useEffect(() => { - setNumCalls(0); - }, [timeoutValue, enabled]); + React.useEffect(() => { + setNumCalls(0); + }, [timeoutValue, enabled]); - return ( -
- Has fired: {numCalls.toString()} -
- Status: {status} -
- { - setTimeoutValue(Number(e.target.value)); - }} - /> - - - -
- ); + return ( +
+ Has fired: {numCalls.toString()} +
+ Status: {status} +
+ { + setTimeoutValue(Number(e.target.value)); + }} + /> + + + +
+ ); } diff --git a/src/useTimeoutEffect/__docs__/story.mdx b/src/useTimeoutEffect/__docs__/story.mdx index 3ecbb8726..423b6a3ab 100644 --- a/src/useTimeoutEffect/__docs__/story.mdx +++ b/src/useTimeoutEffect/__docs__/story.mdx @@ -17,7 +17,7 @@ Like `setTimeout` but in the form of a react hook. #### Example - + ## Reference diff --git a/src/useTimeoutEffect/__tests__/dom.ts b/src/useTimeoutEffect/__tests__/dom.ts index 0b4613d99..0497ef091 100644 --- a/src/useTimeoutEffect/__tests__/dom.ts +++ b/src/useTimeoutEffect/__tests__/dom.ts @@ -2,132 +2,132 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useTimeoutEffect } from '../..'; describe('useTimeoutEffect', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - - beforeEach(() => { - jest.clearAllTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - it('should be defined', () => { - expect(useTimeoutEffect).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useTimeoutEffect(() => {}, 123)); - expect(result.error).toBeUndefined(); - }); - - it('should set and call function after timeout', () => { - const spy = jest.fn(); - renderHook(() => useTimeoutEffect(spy, 100)); - - jest.advanceTimersByTime(99); - expect(spy).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(1); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it('should set timeout and cancel on unmount', () => { - const spy = jest.fn(); - const { unmount } = renderHook(() => useTimeoutEffect(spy, 100)); - - jest.advanceTimersByTime(99); - expect(spy).not.toHaveBeenCalled(); - unmount(); - jest.advanceTimersByTime(1); - expect(spy).not.toHaveBeenCalled(); - }); - - it('should reset timeout in delay change', () => { - const spy = jest.fn(); - const { rerender } = renderHook(({ delay }) => useTimeoutEffect(spy, delay), { - initialProps: { delay: 100 }, - }); - - jest.advanceTimersByTime(99); - expect(spy).not.toHaveBeenCalled(); - - rerender({ delay: 50 }); - jest.advanceTimersByTime(49); - expect(spy).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(1); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it('should not reset timeout in callback change', () => { - const spy = jest.fn(); - const { rerender } = renderHook<{ callback: () => void }, void>( - ({ callback }) => useTimeoutEffect(callback, 100), - { - initialProps: { callback() {} }, - } - ); - - jest.advanceTimersByTime(99); - expect(spy).not.toHaveBeenCalled(); - - rerender({ callback: () => spy() }); - jest.advanceTimersByTime(1); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it('should cancel timeout if delay is undefined', () => { - const spy = jest.fn(); - const { rerender } = renderHook<{ delay: number | undefined }, void>( - ({ delay }) => useTimeoutEffect(spy, delay), - { - initialProps: { delay: 100 }, - } - ); - - jest.advanceTimersByTime(99); - expect(spy).not.toHaveBeenCalled(); - - rerender({ delay: undefined }); - jest.advanceTimersByTime(2000); - expect(spy).not.toHaveBeenCalled(); - }); - - it('should not cancel timeout if delay is 0', () => { - const spy = jest.fn(); - renderHook(() => useTimeoutEffect(spy, 0)); - - jest.advanceTimersByTime(1); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it('should cancel timeout if cancel method is called', () => { - const spy = jest.fn(); - const { result } = renderHook(() => useTimeoutEffect(spy, 100)); - - jest.advanceTimersByTime(99); - expect(spy).not.toHaveBeenCalled(); - - result.current[0](); - jest.advanceTimersByTime(1); - expect(spy).not.toHaveBeenCalled(); - }); - - it('should reset timeout if reset method is called', () => { - const spy = jest.fn(); - const { result } = renderHook(() => useTimeoutEffect(spy, 100)); - - jest.advanceTimersByTime(99); - expect(spy).not.toHaveBeenCalled(); - - result.current[1](); - jest.advanceTimersByTime(1); - expect(spy).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(100); - expect(spy).toHaveBeenCalledTimes(1); - }); + beforeAll(() => { + jest.useFakeTimers(); + }); + + beforeEach(() => { + jest.clearAllTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should be defined', () => { + expect(useTimeoutEffect).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useTimeoutEffect(() => {}, 123)); + expect(result.error).toBeUndefined(); + }); + + it('should set and call function after timeout', () => { + const spy = jest.fn(); + renderHook(() => useTimeoutEffect(spy, 100)); + + jest.advanceTimersByTime(99); + expect(spy).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should set timeout and cancel on unmount', () => { + const spy = jest.fn(); + const { unmount } = renderHook(() => useTimeoutEffect(spy, 100)); + + jest.advanceTimersByTime(99); + expect(spy).not.toHaveBeenCalled(); + unmount(); + jest.advanceTimersByTime(1); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should reset timeout in delay change', () => { + const spy = jest.fn(); + const { rerender } = renderHook(({ delay }) => useTimeoutEffect(spy, delay), { + initialProps: { delay: 100 }, + }); + + jest.advanceTimersByTime(99); + expect(spy).not.toHaveBeenCalled(); + + rerender({ delay: 50 }); + jest.advanceTimersByTime(49); + expect(spy).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should not reset timeout in callback change', () => { + const spy = jest.fn(); + const { rerender } = renderHook<{ callback: () => void }, void>( + ({ callback }) => useTimeoutEffect(callback, 100), + { + initialProps: { callback() {} }, + } + ); + + jest.advanceTimersByTime(99); + expect(spy).not.toHaveBeenCalled(); + + rerender({ callback: () => spy() }); + jest.advanceTimersByTime(1); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should cancel timeout if delay is undefined', () => { + const spy = jest.fn(); + const { rerender } = renderHook<{ delay: number | undefined }, void>( + ({ delay }) => useTimeoutEffect(spy, delay), + { + initialProps: { delay: 100 }, + } + ); + + jest.advanceTimersByTime(99); + expect(spy).not.toHaveBeenCalled(); + + rerender({ delay: undefined }); + jest.advanceTimersByTime(2000); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should not cancel timeout if delay is 0', () => { + const spy = jest.fn(); + renderHook(() => useTimeoutEffect(spy, 0)); + + jest.advanceTimersByTime(1); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should cancel timeout if cancel method is called', () => { + const spy = jest.fn(); + const { result } = renderHook(() => useTimeoutEffect(spy, 100)); + + jest.advanceTimersByTime(99); + expect(spy).not.toHaveBeenCalled(); + + result.current[0](); + jest.advanceTimersByTime(1); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should reset timeout if reset method is called', () => { + const spy = jest.fn(); + const { result } = renderHook(() => useTimeoutEffect(spy, 100)); + + jest.advanceTimersByTime(99); + expect(spy).not.toHaveBeenCalled(); + + result.current[1](); + jest.advanceTimersByTime(1); + expect(spy).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(100); + expect(spy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/useTimeoutEffect/__tests__/ssr.ts b/src/useTimeoutEffect/__tests__/ssr.ts index 9ee0d5284..511e10845 100644 --- a/src/useTimeoutEffect/__tests__/ssr.ts +++ b/src/useTimeoutEffect/__tests__/ssr.ts @@ -2,32 +2,32 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useTimeoutEffect } from '../..'; describe('useTimeoutEffect', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); + beforeAll(() => { + jest.useFakeTimers(); + }); - beforeEach(() => { - jest.clearAllTimers(); - }); + beforeEach(() => { + jest.clearAllTimers(); + }); - afterAll(() => { - jest.useRealTimers(); - }); + afterAll(() => { + jest.useRealTimers(); + }); - it('should be defined', () => { - expect(useTimeoutEffect).toBeDefined(); - }); + it('should be defined', () => { + expect(useTimeoutEffect).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useTimeoutEffect(() => {}, 123)); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useTimeoutEffect(() => {}, 123)); + expect(result.error).toBeUndefined(); + }); - it('should not invoke callback after timeout', () => { - const spy = jest.fn(); - renderHook(() => useTimeoutEffect(spy, 100)); + it('should not invoke callback after timeout', () => { + const spy = jest.fn(); + renderHook(() => useTimeoutEffect(spy, 100)); - jest.advanceTimersByTime(100); - expect(spy).not.toHaveBeenCalled(); - }); + jest.advanceTimersByTime(100); + expect(spy).not.toHaveBeenCalled(); + }); }); diff --git a/src/useTimeoutEffect/index.ts b/src/useTimeoutEffect/index.ts index ff9e2992b..4708cb19c 100644 --- a/src/useTimeoutEffect/index.ts +++ b/src/useTimeoutEffect/index.ts @@ -4,9 +4,9 @@ import { useSyncedRef } from '../useSyncedRef'; type TimeoutID = ReturnType | null; const cancelTimeout = (id: TimeoutID) => { - if (id) { - clearTimeout(id); - } + if (id) { + clearTimeout(id); + } }; /** @@ -19,30 +19,30 @@ const cancelTimeout = (id: TimeoutID) => { * that it will be set as new after the change. */ export function useTimeoutEffect( - callback: () => void, - ms?: number + callback: () => void, + ms?: number ): [cancel: () => void, reset: () => void] { - const cbRef = useSyncedRef(callback); - const msRef = useSyncedRef(ms); - const timeoutIdRef = useRef(null); + const cbRef = useSyncedRef(callback); + const msRef = useSyncedRef(ms); + const timeoutIdRef = useRef(null); - const cancel = useCallback(() => { - cancelTimeout(timeoutIdRef.current); - }, []); + const cancel = useCallback(() => { + cancelTimeout(timeoutIdRef.current); + }, []); - const reset = useCallback(() => { - if (msRef.current === undefined) return; + const reset = useCallback(() => { + if (msRef.current === undefined) return; - cancel(); - timeoutIdRef.current = setTimeout(() => { - cbRef.current(); - }, msRef.current); - }, []); + cancel(); + timeoutIdRef.current = setTimeout(() => { + cbRef.current(); + }, msRef.current); + }, []); - useEffect(() => { - reset(); - return cancel; - }, [ms]); + useEffect(() => { + reset(); + return cancel; + }, [ms]); - return [cancel, reset]; + return [cancel, reset]; } diff --git a/src/useToggle/__docs__/example.stories.tsx b/src/useToggle/__docs__/example.stories.tsx index c6a92dfc8..0f41fed68 100644 --- a/src/useToggle/__docs__/example.stories.tsx +++ b/src/useToggle/__docs__/example.stories.tsx @@ -2,14 +2,14 @@ import * as React from 'react'; import { useToggle } from '../..'; export function Example() { - const [isToggled, toggle] = useToggle(true); + const [isToggled, toggle] = useToggle(true); - return ( -
-
{isToggled ? 'The toggle is on' : 'The toggle is off'}
- -
- ); + return ( +
+
{isToggled ? 'The toggle is on' : 'The toggle is off'}
+ +
+ ); } diff --git a/src/useToggle/__docs__/story.mdx b/src/useToggle/__docs__/story.mdx index b71925181..aeac01871 100644 --- a/src/useToggle/__docs__/story.mdx +++ b/src/useToggle/__docs__/story.mdx @@ -17,20 +17,20 @@ can be changed by setting 2nd parameter to `false`. #### Example - + ## Reference ```ts export function useToggle( - initialState: InitialState, - ignoreReactEvents?: true + initialState: InitialState, + ignoreReactEvents?: true ): [boolean, (nextState?: NextState | BaseSyntheticEvent) => void]; export function useToggle( - initialState: InitialState, - ignoreReactEvents: false + initialState: InitialState, + ignoreReactEvents: false ): [boolean, (nextState?: NextState) => void]; ``` diff --git a/src/useToggle/__tests__/dom.ts b/src/useToggle/__tests__/dom.ts index d5b189f0d..1d06662a6 100644 --- a/src/useToggle/__tests__/dom.ts +++ b/src/useToggle/__tests__/dom.ts @@ -3,97 +3,97 @@ import { type BaseSyntheticEvent, useRef } from 'react'; import { useToggle } from '../..'; describe('useToggle', () => { - it('should be defined', () => { - expect(useToggle).toBeDefined(); - }); - - it('should default to false', () => { - const { result } = renderHook(() => useToggle()); - - expect(result.current[0]).toBe(false); - }); - - it('should be instantiatable with value', () => { - let { result } = renderHook(() => useToggle(true)); - expect(result.current[0]).toBe(true); - - result = renderHook(() => useToggle(() => true)).result; - expect(result.current[0]).toBe(true); - - result = renderHook(() => useToggle(() => false)).result; - expect(result.current[0]).toBe(false); - }); - - it('should change state to the opposite when toggler called without args or undefined', () => { - const { result } = renderHook(() => useToggle()); - act(() => { - result.current[1](); - }); - expect(result.current[0]).toBe(true); - - act(() => { - result.current[1](); - }); - expect(result.current[0]).toBe(false); - }); - - it('should not rerender when toggler called with same value', () => { - const { result } = renderHook(() => { - const cnt = useRef(0); - - return [...useToggle(), ++cnt.current] as const; - }); - expect(result.current[0]).toBe(false); - expect(result.current[2]).toBe(1); - - act(() => { - result.current[1](false); - }); - expect(result.current[2]).toBe(1); - - act(() => { - result.current[1](false); - }); - expect(result.current[2]).toBe(1); - }); - - it('should change state to one that passed to toggler', () => { - const { result } = renderHook(() => useToggle(false, false)); - act(() => { - result.current[1](false); - }); - expect(result.current[0]).toBe(false); - - act(() => { - result.current[1](true); - }); - expect(result.current[0]).toBe(true); - - act(() => { - result.current[1](() => false); - }); - expect(result.current[0]).toBe(false); - - act(() => { - result.current[1](() => true); - }); - expect(result.current[0]).toBe(true); - }); - - it('should not account react events', () => { - const { result } = renderHook(() => useToggle()); - - act(() => { - result.current[1]({ _reactName: 'abcdef' } as unknown as BaseSyntheticEvent); - - result.current[1]({ _reactName: 'abcdef' } as unknown as BaseSyntheticEvent); - }); - expect(result.current[0]).toBe(false); - - act(() => { - // eslint-disable-next-line @typescript-eslint/no-extraneous-class - result.current[1](new (class SyntheticBaseEvent {})() as unknown as BaseSyntheticEvent); - }); - expect(result.current[0]).toBe(true); - }); + it('should be defined', () => { + expect(useToggle).toBeDefined(); + }); + + it('should default to false', () => { + const { result } = renderHook(() => useToggle()); + + expect(result.current[0]).toBe(false); + }); + + it('should be instantiatable with value', () => { + let { result } = renderHook(() => useToggle(true)); + expect(result.current[0]).toBe(true); + + result = renderHook(() => useToggle(() => true)).result; + expect(result.current[0]).toBe(true); + + result = renderHook(() => useToggle(() => false)).result; + expect(result.current[0]).toBe(false); + }); + + it('should change state to the opposite when toggler called without args or undefined', () => { + const { result } = renderHook(() => useToggle()); + act(() => { + result.current[1](); + }); + expect(result.current[0]).toBe(true); + + act(() => { + result.current[1](); + }); + expect(result.current[0]).toBe(false); + }); + + it('should not rerender when toggler called with same value', () => { + const { result } = renderHook(() => { + const cnt = useRef(0); + + return [...useToggle(), ++cnt.current] as const; + }); + expect(result.current[0]).toBe(false); + expect(result.current[2]).toBe(1); + + act(() => { + result.current[1](false); + }); + expect(result.current[2]).toBe(1); + + act(() => { + result.current[1](false); + }); + expect(result.current[2]).toBe(1); + }); + + it('should change state to one that passed to toggler', () => { + const { result } = renderHook(() => useToggle(false, false)); + act(() => { + result.current[1](false); + }); + expect(result.current[0]).toBe(false); + + act(() => { + result.current[1](true); + }); + expect(result.current[0]).toBe(true); + + act(() => { + result.current[1](() => false); + }); + expect(result.current[0]).toBe(false); + + act(() => { + result.current[1](() => true); + }); + expect(result.current[0]).toBe(true); + }); + + it('should not account react events', () => { + const { result } = renderHook(() => useToggle()); + + act(() => { + result.current[1]({ _reactName: 'abcdef' } as unknown as BaseSyntheticEvent); + + result.current[1]({ _reactName: 'abcdef' } as unknown as BaseSyntheticEvent); + }); + expect(result.current[0]).toBe(false); + + act(() => { + // eslint-disable-next-line @typescript-eslint/no-extraneous-class + result.current[1](new (class SyntheticBaseEvent {})() as unknown as BaseSyntheticEvent); + }); + expect(result.current[0]).toBe(true); + }); }); diff --git a/src/useToggle/__tests__/ssr.ts b/src/useToggle/__tests__/ssr.ts index 80f0a014d..dc3757908 100644 --- a/src/useToggle/__tests__/ssr.ts +++ b/src/useToggle/__tests__/ssr.ts @@ -2,37 +2,37 @@ import { act, renderHook } from '@testing-library/react-hooks/server'; import { useToggle } from '../..'; describe('useToggle', () => { - it('should be defined', () => { - expect(useToggle).toBeDefined(); - }); - - it('should default to false', () => { - const { result } = renderHook(() => useToggle()); - - expect(result.current[0]).toBe(false); - }); - - it('should be instantiatable with value', () => { - let { result } = renderHook(() => useToggle(true)); - expect(result.current[0]).toBe(true); - - result = renderHook(() => useToggle(() => true)).result; - expect(result.current[0]).toBe(true); - - result = renderHook(() => useToggle(() => false)).result; - expect(result.current[0]).toBe(false); - }); - - it('should not change if toggler called', () => { - const { result } = renderHook(() => useToggle()); - act(() => { - result.current[1](); - }); - expect(result.current[0]).toBe(false); - - act(() => { - result.current[1](true); - }); - expect(result.current[0]).toBe(false); - }); + it('should be defined', () => { + expect(useToggle).toBeDefined(); + }); + + it('should default to false', () => { + const { result } = renderHook(() => useToggle()); + + expect(result.current[0]).toBe(false); + }); + + it('should be instantiatable with value', () => { + let { result } = renderHook(() => useToggle(true)); + expect(result.current[0]).toBe(true); + + result = renderHook(() => useToggle(() => true)).result; + expect(result.current[0]).toBe(true); + + result = renderHook(() => useToggle(() => false)).result; + expect(result.current[0]).toBe(false); + }); + + it('should not change if toggler called', () => { + const { result } = renderHook(() => useToggle()); + act(() => { + result.current[1](); + }); + expect(result.current[0]).toBe(false); + + act(() => { + result.current[1](true); + }); + expect(result.current[0]).toBe(false); + }); }); diff --git a/src/useToggle/index.ts b/src/useToggle/index.ts index 7944250a3..fb130703c 100644 --- a/src/useToggle/index.ts +++ b/src/useToggle/index.ts @@ -3,12 +3,12 @@ import { useSyncedRef } from '../useSyncedRef'; import { type InitialState, type NextState, resolveHookState } from '../util/resolveHookState'; export function useToggle( - initialState: InitialState, - ignoreReactEvents: false + initialState: InitialState, + ignoreReactEvents: false ): [boolean, (nextState?: NextState) => void]; export function useToggle( - initialState?: InitialState, - ignoreReactEvents?: true + initialState?: InitialState, + ignoreReactEvents?: true ): [boolean, (nextState?: NextState | BaseSyntheticEvent) => void]; /** @@ -19,32 +19,32 @@ export function useToggle( * such behaviour can be changed by setting 2nd parameter to `false`. */ export function useToggle( - initialState: InitialState = false, - ignoreReactEvents = true + initialState: InitialState = false, + ignoreReactEvents = true ): [boolean, (nextState?: NextState | BaseSyntheticEvent) => void] { - // We don't use useReducer (which would end up with less code), because exposed - // action does not provide functional updates feature. - // Therefore, we have to create and expose our own state setter with - // toggle logic. - const [state, setState] = useState(initialState); - const ignoreReactEventsRef = useSyncedRef(ignoreReactEvents); + // We don't use useReducer (which would end up with less code), because exposed + // action does not provide functional updates feature. + // Therefore, we have to create and expose our own state setter with + // toggle logic. + const [state, setState] = useState(initialState); + const ignoreReactEventsRef = useSyncedRef(ignoreReactEvents); - return [ - state, - useCallback((nextState) => { - setState((prevState) => { - if ( - nextState === undefined || - (ignoreReactEventsRef.current && - typeof nextState === 'object' && - (nextState.constructor.name === 'SyntheticBaseEvent' || - typeof (nextState as any)._reactName === 'string')) - ) { - return !prevState; - } + return [ + state, + useCallback((nextState) => { + setState((prevState) => { + if ( + nextState === undefined || + (ignoreReactEventsRef.current && + typeof nextState === 'object' && + (nextState.constructor.name === 'SyntheticBaseEvent' || + typeof (nextState as any)._reactName === 'string')) + ) { + return !prevState; + } - return Boolean(resolveHookState(nextState, prevState)); - }); - }, []), - ]; + return Boolean(resolveHookState(nextState, prevState)); + }); + }, []), + ]; } diff --git a/src/useUnmountEffect/__docs__/example.stories.tsx b/src/useUnmountEffect/__docs__/example.stories.tsx index ca814cccb..a5e677f21 100644 --- a/src/useUnmountEffect/__docs__/example.stories.tsx +++ b/src/useUnmountEffect/__docs__/example.stories.tsx @@ -2,27 +2,27 @@ import * as React from 'react'; import { useToggle, useUnmountEffect } from '../..'; export function Example() { - const [isToggled, toggle] = useToggle(false); + const [isToggled, toggle] = useToggle(false); - function ToggledComponent() { - useUnmountEffect(() => { - // eslint-disable-next-line no-alert - alert('UNMOUNTED'); - }); + function ToggledComponent() { + useUnmountEffect(() => { + // eslint-disable-next-line no-alert + alert('UNMOUNTED'); + }); - return

Unmount me

; - } + return

Unmount me

; + } - return ( -
- {' '} - {isToggled && } -
- ); + return ( +
+ {' '} + {isToggled && } +
+ ); } diff --git a/src/useUnmountEffect/__docs__/story.mdx b/src/useUnmountEffect/__docs__/story.mdx index 434292a62..61a40fa1b 100644 --- a/src/useUnmountEffect/__docs__/story.mdx +++ b/src/useUnmountEffect/__docs__/story.mdx @@ -11,7 +11,7 @@ Run effect only when component is unmounted. #### Example - + ## Reference diff --git a/src/useUnmountEffect/__tests__/dom.ts b/src/useUnmountEffect/__tests__/dom.ts index 56535d7ff..c35633a02 100644 --- a/src/useUnmountEffect/__tests__/dom.ts +++ b/src/useUnmountEffect/__tests__/dom.ts @@ -2,39 +2,39 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useUnmountEffect } from '../..'; describe('useUnmountEffect', () => { - it('should call effector only when component unmounted', () => { - const spy = jest.fn(); + it('should call effector only when component unmounted', () => { + const spy = jest.fn(); - const { result, rerender, unmount } = renderHook(() => { - useUnmountEffect(spy); - }); + const { result, rerender, unmount } = renderHook(() => { + useUnmountEffect(spy); + }); - expect(result.current).toBe(undefined); - expect(spy).toHaveBeenCalledTimes(0); + expect(result.current).toBe(undefined); + expect(spy).toHaveBeenCalledTimes(0); - rerender(); - unmount(); + rerender(); + unmount(); - expect(spy).toHaveBeenCalledTimes(1); - }); + expect(spy).toHaveBeenCalledTimes(1); + }); - it('should call effect even if it has been updated', () => { - const spy = jest.fn(); + it('should call effect even if it has been updated', () => { + const spy = jest.fn(); - const { rerender, unmount } = renderHook<{ fn: () => void }, void>( - ({ fn }) => { - useUnmountEffect(fn); - }, - { - initialProps: { - fn() {}, - }, - } - ); + const { rerender, unmount } = renderHook<{ fn: () => void }, void>( + ({ fn }) => { + useUnmountEffect(fn); + }, + { + initialProps: { + fn() {}, + }, + } + ); - rerender({ fn: spy }); - unmount(); + rerender({ fn: spy }); + unmount(); - expect(spy).toHaveBeenCalled(); - }); + expect(spy).toHaveBeenCalled(); + }); }); diff --git a/src/useUnmountEffect/__tests__/ssr.ts b/src/useUnmountEffect/__tests__/ssr.ts index 449a806fc..df593209e 100644 --- a/src/useUnmountEffect/__tests__/ssr.ts +++ b/src/useUnmountEffect/__tests__/ssr.ts @@ -2,13 +2,13 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useUnmountEffect } from '../..'; describe('useUnmountEffect', () => { - it('should call effector only when component unmounted', () => { - const spy = jest.fn(); + it('should call effector only when component unmounted', () => { + const spy = jest.fn(); - renderHook(() => { - useUnmountEffect(spy); - }); + renderHook(() => { + useUnmountEffect(spy); + }); - expect(spy).toHaveBeenCalledTimes(0); - }); + expect(spy).toHaveBeenCalledTimes(0); + }); }); diff --git a/src/useUnmountEffect/index.ts b/src/useUnmountEffect/index.ts index f8d8439bc..026e6434e 100644 --- a/src/useUnmountEffect/index.ts +++ b/src/useUnmountEffect/index.ts @@ -7,13 +7,13 @@ import { useSyncedRef } from '../useSyncedRef'; * @param effect Effector to run on unmount */ export function useUnmountEffect(effect: CallableFunction): void { - const effectRef = useSyncedRef(effect); + const effectRef = useSyncedRef(effect); - useEffect( - () => () => { - effectRef.current(); - }, + useEffect( + () => () => { + effectRef.current(); + }, - [] - ); + [] + ); } diff --git a/src/useUpdateEffect/__docs__/example.stories.tsx b/src/useUpdateEffect/__docs__/example.stories.tsx index dbadfafb8..09666551b 100644 --- a/src/useUpdateEffect/__docs__/example.stories.tsx +++ b/src/useUpdateEffect/__docs__/example.stories.tsx @@ -3,34 +3,34 @@ import { useState } from 'react'; import { useRerender, useUpdateEffect } from '../..'; export function Example() { - const [count, setCount] = useState(1); - const [isUpdated, setIsUpdated] = useState(false); - const rerender = useRerender(); + const [count, setCount] = useState(1); + const [isUpdated, setIsUpdated] = useState(false); + const rerender = useRerender(); - useUpdateEffect(() => { - setIsUpdated(true); - }, [count]); + useUpdateEffect(() => { + setIsUpdated(true); + }, [count]); - return ( -
-
- Is counter updated: - {isUpdated ? 'yes' : 'no'} -
- {' '} - -
- ); + return ( +
+
+ Is counter updated: + {isUpdated ? 'yes' : 'no'} +
+ {' '} + +
+ ); } diff --git a/src/useUpdateEffect/__docs__/story.mdx b/src/useUpdateEffect/__docs__/story.mdx index 1a080e049..841fd0e4c 100644 --- a/src/useUpdateEffect/__docs__/story.mdx +++ b/src/useUpdateEffect/__docs__/story.mdx @@ -11,7 +11,7 @@ Effect hook that ignores the first render (not invoked on mount) #### Example - + ## Reference diff --git a/src/useUpdateEffect/__tests__/dom.ts b/src/useUpdateEffect/__tests__/dom.ts index dd4af8499..3c1d8ba76 100644 --- a/src/useUpdateEffect/__tests__/dom.ts +++ b/src/useUpdateEffect/__tests__/dom.ts @@ -2,49 +2,49 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useUpdateEffect } from '../..'; describe('useUpdateEffect', () => { - it('should call effector only on updates (after first render)', () => { - const spy = jest.fn(); + it('should call effector only on updates (after first render)', () => { + const spy = jest.fn(); - const { rerender, unmount } = renderHook(() => { - useUpdateEffect(spy); - }); + const { rerender, unmount } = renderHook(() => { + useUpdateEffect(spy); + }); - expect(spy).toHaveBeenCalledTimes(0); + expect(spy).toHaveBeenCalledTimes(0); - rerender(); - expect(spy).toHaveBeenCalledTimes(1); + rerender(); + expect(spy).toHaveBeenCalledTimes(1); - rerender(); - expect(spy).toHaveBeenCalledTimes(2); + rerender(); + expect(spy).toHaveBeenCalledTimes(2); - unmount(); - expect(spy).toHaveBeenCalledTimes(2); - }); + unmount(); + expect(spy).toHaveBeenCalledTimes(2); + }); - it('should accept dependencies as useEffect', () => { - const spy = jest.fn(); + it('should accept dependencies as useEffect', () => { + const spy = jest.fn(); - const { rerender, unmount } = renderHook( - ({ deps }) => { - useUpdateEffect(spy, deps); - }, - { - initialProps: { deps: [1, 2, 3] }, - } - ); + const { rerender, unmount } = renderHook( + ({ deps }) => { + useUpdateEffect(spy, deps); + }, + { + initialProps: { deps: [1, 2, 3] }, + } + ); - expect(spy).toHaveBeenCalledTimes(0); + expect(spy).toHaveBeenCalledTimes(0); - rerender(); - expect(spy).toHaveBeenCalledTimes(0); + rerender(); + expect(spy).toHaveBeenCalledTimes(0); - rerender({ deps: [1, 2, 4] }); - expect(spy).toHaveBeenCalledTimes(1); + rerender({ deps: [1, 2, 4] }); + expect(spy).toHaveBeenCalledTimes(1); - rerender({ deps: [1, 2, 4] }); - expect(spy).toHaveBeenCalledTimes(1); + rerender({ deps: [1, 2, 4] }); + expect(spy).toHaveBeenCalledTimes(1); - unmount(); - expect(spy).toHaveBeenCalledTimes(1); - }); + unmount(); + expect(spy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/useUpdateEffect/__tests__/ssr.ts b/src/useUpdateEffect/__tests__/ssr.ts index 9014fc56c..144bdbbf4 100644 --- a/src/useUpdateEffect/__tests__/ssr.ts +++ b/src/useUpdateEffect/__tests__/ssr.ts @@ -2,13 +2,13 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useUpdateEffect } from '../..'; describe('useUpdateEffect', () => { - it('should not call effector on mount', () => { - const spy = jest.fn(); + it('should not call effector on mount', () => { + const spy = jest.fn(); - renderHook(() => { - useUpdateEffect(spy); - }); + renderHook(() => { + useUpdateEffect(spy); + }); - expect(spy).toHaveBeenCalledTimes(0); - }); + expect(spy).toHaveBeenCalledTimes(0); + }); }); diff --git a/src/useUpdateEffect/index.ts b/src/useUpdateEffect/index.ts index 7def17b41..d4f692a9d 100644 --- a/src/useUpdateEffect/index.ts +++ b/src/useUpdateEffect/index.ts @@ -9,7 +9,7 @@ import { noop } from '../util/const'; * @param deps Dependencies list, as for `useEffect` hook */ export function useUpdateEffect(effect: EffectCallback, deps?: DependencyList): void { - const isFirstMount = useFirstMountState(); + const isFirstMount = useFirstMountState(); - useEffect(isFirstMount ? noop : effect, deps); + useEffect(isFirstMount ? noop : effect, deps); } diff --git a/src/useValidator/__docs__/example.tsx b/src/useValidator/__docs__/example.tsx index 585bd061a..2f6eb5a40 100644 --- a/src/useValidator/__docs__/example.tsx +++ b/src/useValidator/__docs__/example.tsx @@ -3,43 +3,43 @@ import { useState } from 'react'; import { type ValidatorDeferred, useDebouncedCallback, useValidator } from '../..'; export function Example() { - const [text, setText] = useState(''); + const [text, setText] = useState(''); - // As deferred validator is unable to infer the type of validity - // state - we should define it ourself - type TextValidityState = { isValid: boolean | undefined; error: Error | undefined }; + // As deferred validator is unable to infer the type of validity + // state - we should define it ourself + type TextValidityState = { isValid: boolean | undefined; error: Error | undefined }; - // Debounced callback is deferred callback so we should use deferred type - // of validator (the one that receives dispatcher as an argument) - const validator = useDebouncedCallback>( - (d) => { - const isValid = !text.length || text.length % 2 === 1; + // Debounced callback is deferred callback so we should use deferred type + // of validator (the one that receives dispatcher as an argument) + const validator = useDebouncedCallback>( + (d) => { + const isValid = !text.length || text.length % 2 === 1; - d({ - isValid, - error: isValid ? undefined : new Error('text length should be an odd length'), - }); - }, - [text], - 150 - ); + d({ + isValid, + error: isValid ? undefined : new Error('text length should be an odd length'), + }); + }, + [text], + 150 + ); - // Validity state type if inferred from validator - const [validity] = useValidator(validator, [validator]); + // Validity state type if inferred from validator + const [validity] = useValidator(validator, [validator]); - return ( -
-
The input below is only valid if it has an odd number of characters
-
+ return ( +
+
The input below is only valid if it has an odd number of characters
+
- {validity.isValid === false &&
{validity.error?.message}
} - { - setText(ev.target.value); - }} - /> -
- ); + {validity.isValid === false &&
{validity.error?.message}
} + { + setText(ev.target.value); + }} + /> +
+ ); } diff --git a/src/useValidator/__docs__/story.mdx b/src/useValidator/__docs__/story.mdx index e5279e903..df79656ad 100644 --- a/src/useValidator/__docs__/story.mdx +++ b/src/useValidator/__docs__/story.mdx @@ -17,7 +17,7 @@ Performs validation when any of provided dependencies has changed. #### Example - + ## Reference diff --git a/src/useValidator/__tests__/dom.ts b/src/useValidator/__tests__/dom.ts index 9dd3e312f..af565670f 100644 --- a/src/useValidator/__tests__/dom.ts +++ b/src/useValidator/__tests__/dom.ts @@ -2,68 +2,68 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; import { useValidator, type UseValidatorReturn } from '../..'; describe('useValidator', () => { - it('should be defined', () => { - expect(useValidator).toBeDefined(); - }); + it('should be defined', () => { + expect(useValidator).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useValidator(() => ({ isValid: false }), [])); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useValidator(() => ({ isValid: false }), [])); + expect(result.error).toBeUndefined(); + }); - it('should return undefined validity on first render', () => { - const { result } = renderHook(() => useValidator(() => ({ isValid: true }), [])); - expect((result.all[0] as UseValidatorReturn<{ isValid: boolean }>)[0].isValid).toBeUndefined(); - }); + it('should return undefined validity on first render', () => { + const { result } = renderHook(() => useValidator(() => ({ isValid: true }), [])); + expect((result.all[0] as UseValidatorReturn<{ isValid: boolean }>)[0].isValid).toBeUndefined(); + }); - it('should apply initial state parameter', () => { - const { result } = renderHook(() => - useValidator(() => ({ isValid: true }), [], { isValid: true }) - ); - expect((result.all[0] as UseValidatorReturn<{ isValid: boolean }>)[0].isValid).toBe(true); - }); + it('should apply initial state parameter', () => { + const { result } = renderHook(() => + useValidator(() => ({ isValid: true }), [], { isValid: true }) + ); + expect((result.all[0] as UseValidatorReturn<{ isValid: boolean }>)[0].isValid).toBe(true); + }); - it('should call validator on first render', () => { - const spy = jest.fn(() => ({ isValid: true })); - const { result } = renderHook(() => useValidator(spy, [])); - expect(spy).toHaveBeenCalledTimes(1); - expect(result.current[0].isValid).toBe(true); - }); + it('should call validator on first render', () => { + const spy = jest.fn(() => ({ isValid: true })); + const { result } = renderHook(() => useValidator(spy, [])); + expect(spy).toHaveBeenCalledTimes(1); + expect(result.current[0].isValid).toBe(true); + }); - it('should call validator on if deps changed', () => { - const spy = jest.fn(() => ({ isValid: true })); - const { rerender } = renderHook(({ dep }) => useValidator(spy, [dep]), { - initialProps: { dep: 1 }, - }); - expect(spy).toHaveBeenCalledTimes(1); + it('should call validator on if deps changed', () => { + const spy = jest.fn(() => ({ isValid: true })); + const { rerender } = renderHook(({ dep }) => useValidator(spy, [dep]), { + initialProps: { dep: 1 }, + }); + expect(spy).toHaveBeenCalledTimes(1); - rerender({ dep: 2 }); - expect(spy).toHaveBeenCalledTimes(2); - }); + rerender({ dep: 2 }); + expect(spy).toHaveBeenCalledTimes(2); + }); - it('should call validator on revalidator invocation', () => { - const spy = jest.fn(() => ({ isValid: true })); - const { result } = renderHook(({ dep }) => useValidator(spy, [dep]), { - initialProps: { dep: 1 }, - }); - expect(spy).toHaveBeenCalledTimes(1); + it('should call validator on revalidator invocation', () => { + const spy = jest.fn(() => ({ isValid: true })); + const { result } = renderHook(({ dep }) => useValidator(spy, [dep]), { + initialProps: { dep: 1 }, + }); + expect(spy).toHaveBeenCalledTimes(1); - act(() => { - result.current[1](); - }); - expect(spy).toHaveBeenCalledTimes(2); - }); + act(() => { + result.current[1](); + }); + expect(spy).toHaveBeenCalledTimes(2); + }); - it('should pass the validity setter if validator expects it', () => { - const { result } = renderHook(() => - useValidator<{ isValid: false; customError: Error }>((d) => { - d({ isValid: false, customError: new Error('this is custom error') }); - }, []) - ); + it('should pass the validity setter if validator expects it', () => { + const { result } = renderHook(() => + useValidator<{ isValid: false; customError: Error }>((d) => { + d({ isValid: false, customError: new Error('this is custom error') }); + }, []) + ); - expect(result.current[0]).toStrictEqual({ - isValid: false, - customError: new Error('this is custom error'), - }); - }); + expect(result.current[0]).toStrictEqual({ + isValid: false, + customError: new Error('this is custom error'), + }); + }); }); diff --git a/src/useValidator/__tests__/ssr.ts b/src/useValidator/__tests__/ssr.ts index 4ba21830f..bd2a4b141 100644 --- a/src/useValidator/__tests__/ssr.ts +++ b/src/useValidator/__tests__/ssr.ts @@ -2,23 +2,23 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useValidator } from '../..'; describe('useValidator', () => { - it('should be defined', () => { - expect(useValidator).toBeDefined(); - }); + it('should be defined', () => { + expect(useValidator).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useValidator(() => ({ isValid: false }), [])); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useValidator(() => ({ isValid: false }), [])); + expect(result.error).toBeUndefined(); + }); - it('should return undefined validity on first render', () => { - const { result } = renderHook(() => useValidator(() => ({ isValid: true }), [])); - expect(result.current[0].isValid).toBeUndefined(); - }); + it('should return undefined validity on first render', () => { + const { result } = renderHook(() => useValidator(() => ({ isValid: true }), [])); + expect(result.current[0].isValid).toBeUndefined(); + }); - it('should not call validator on first render', () => { - const spy = jest.fn(() => ({ isValid: true })); - renderHook(() => useValidator(spy, [])); - expect(spy).not.toHaveBeenCalled(); - }); + it('should not call validator on first render', () => { + const spy = jest.fn(() => ({ isValid: true })); + renderHook(() => useValidator(spy, [])); + expect(spy).not.toHaveBeenCalled(); + }); }); diff --git a/src/useValidator/index.ts b/src/useValidator/index.ts index 7d7ed15b6..1a6b51f4f 100644 --- a/src/useValidator/index.ts +++ b/src/useValidator/index.ts @@ -3,17 +3,17 @@ import { useSyncedRef } from '../useSyncedRef'; import { type InitialState, type NextState } from '../util/resolveHookState'; export type ValidityState = { - isValid: boolean | undefined; + isValid: boolean | undefined; } & Record; export type ValidatorImmediate = () => V; export type ValidatorDeferred = ( - done: Dispatch> + done: Dispatch> ) => any; export type Validator = - | ValidatorImmediate - | ValidatorDeferred; + | ValidatorImmediate + | ValidatorDeferred; export type UseValidatorReturn = [V, () => void]; @@ -25,27 +25,27 @@ export type UseValidatorReturn = [V, () => void]; * @param initialValidity Initial validity state. */ export function useValidator( - validator: Validator, - deps: DependencyList, - initialValidity: InitialState = { isValid: undefined } as V + validator: Validator, + deps: DependencyList, + initialValidity: InitialState = { isValid: undefined } as V ): UseValidatorReturn { - const [validity, setValidity] = useState(initialValidity); - const validatorRef = useSyncedRef(() => { - if (validator.length) { - validator(setValidity); - } else { - setValidity((validator as ValidatorImmediate)()); - } - }); + const [validity, setValidity] = useState(initialValidity); + const validatorRef = useSyncedRef(() => { + if (validator.length) { + validator(setValidity); + } else { + setValidity((validator as ValidatorImmediate)()); + } + }); - useEffect(() => { - validatorRef.current(); - }, deps); + useEffect(() => { + validatorRef.current(); + }, deps); - return [ - validity, - useCallback(() => { - validatorRef.current(); - }, []), - ]; + return [ + validity, + useCallback(() => { + validatorRef.current(); + }, []), + ]; } diff --git a/src/useVibrate/__docs__/example.stories.tsx b/src/useVibrate/__docs__/example.stories.tsx index c135e4088..d692b1fbd 100644 --- a/src/useVibrate/__docs__/example.stories.tsx +++ b/src/useVibrate/__docs__/example.stories.tsx @@ -2,23 +2,23 @@ import * as React from 'react'; import { useToggle, useVibrate } from '../..'; export function Example() { - const [doVibrate, setDoVibrate] = useToggle(false); + const [doVibrate, setDoVibrate] = useToggle(false); - useVibrate( - doVibrate, - [100, 30, 100, 30, 100, 30, 200, 30, 200, 30, 200, 30, 100, 30, 100, 30, 100], - true - ); + useVibrate( + doVibrate, + [100, 30, 100, 30, 100, 30, 200, 30, 200, 30, 200, 30, 100, 30, 100, 30, 100], + true + ); - return ( -
- -
- ); + return ( +
+ +
+ ); } diff --git a/src/useVibrate/__docs__/story.mdx b/src/useVibrate/__docs__/story.mdx index 5d0af6ed5..797df5aa1 100644 --- a/src/useVibrate/__docs__/story.mdx +++ b/src/useVibrate/__docs__/story.mdx @@ -17,7 +17,7 @@ Provides vibration feedback using the [Vibration API](https://developer.mozilla. #### Example - + ## Reference diff --git a/src/useVibrate/__tests__/dom.ts b/src/useVibrate/__tests__/dom.ts index 632eb29ce..43bd7272d 100644 --- a/src/useVibrate/__tests__/dom.ts +++ b/src/useVibrate/__tests__/dom.ts @@ -2,64 +2,64 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { useVibrate } from '../..'; describe('useVibrate', () => { - const vibrateSpy = jest.spyOn(navigator, 'vibrate'); + const vibrateSpy = jest.spyOn(navigator, 'vibrate'); - beforeEach(() => { - vibrateSpy.mockReset(); - }); + beforeEach(() => { + vibrateSpy.mockReset(); + }); - afterAll(() => { - vibrateSpy.mockRestore(); - }); + afterAll(() => { + vibrateSpy.mockRestore(); + }); - it('should be defined', () => { - expect(useVibrate).toBeDefined(); - }); + it('should be defined', () => { + expect(useVibrate).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useVibrate(true, 100); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useVibrate(true, 100); + }); + expect(result.error).toBeUndefined(); + }); - it('should call navigator.vibrate', () => { - renderHook(() => { - useVibrate(true, [100, 200]); - }); - expect(vibrateSpy).toHaveBeenCalledTimes(1); - expect(vibrateSpy.mock.calls[0][0]).toEqual([100, 200]); - }); + it('should call navigator.vibrate', () => { + renderHook(() => { + useVibrate(true, [100, 200]); + }); + expect(vibrateSpy).toHaveBeenCalledTimes(1); + expect(vibrateSpy.mock.calls[0][0]).toEqual([100, 200]); + }); - it('should call navigator.vibrate(0) on unmount', () => { - const { unmount } = renderHook(() => { - useVibrate(true, [100, 200], true); - }); - unmount(); + it('should call navigator.vibrate(0) on unmount', () => { + const { unmount } = renderHook(() => { + useVibrate(true, [100, 200], true); + }); + unmount(); - expect(vibrateSpy.mock.calls[1][0]).toEqual(0); - }); + expect(vibrateSpy.mock.calls[1][0]).toEqual(0); + }); - it('should vibrate constantly using interval', () => { - jest.useFakeTimers(); - renderHook(() => { - useVibrate(true, 300, true); - }); + it('should vibrate constantly using interval', () => { + jest.useFakeTimers(); + renderHook(() => { + useVibrate(true, 300, true); + }); - expect(vibrateSpy).toHaveBeenCalledTimes(1); - expect(vibrateSpy.mock.calls[0][0]).toEqual(300); + expect(vibrateSpy).toHaveBeenCalledTimes(1); + expect(vibrateSpy.mock.calls[0][0]).toEqual(300); - jest.advanceTimersByTime(299); - expect(vibrateSpy).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(299); + expect(vibrateSpy).toHaveBeenCalledTimes(1); - jest.advanceTimersByTime(1); - expect(vibrateSpy).toHaveBeenCalledTimes(2); - expect(vibrateSpy.mock.calls[1][0]).toEqual(300); + jest.advanceTimersByTime(1); + expect(vibrateSpy).toHaveBeenCalledTimes(2); + expect(vibrateSpy.mock.calls[1][0]).toEqual(300); - jest.advanceTimersByTime(300); - expect(vibrateSpy).toHaveBeenCalledTimes(3); - expect(vibrateSpy.mock.calls[2][0]).toEqual(300); + jest.advanceTimersByTime(300); + expect(vibrateSpy).toHaveBeenCalledTimes(3); + expect(vibrateSpy.mock.calls[2][0]).toEqual(300); - jest.useRealTimers(); - }); + jest.useRealTimers(); + }); }); diff --git a/src/useVibrate/__tests__/ssr.ts b/src/useVibrate/__tests__/ssr.ts index 24a6b5a54..d716208a8 100644 --- a/src/useVibrate/__tests__/ssr.ts +++ b/src/useVibrate/__tests__/ssr.ts @@ -2,14 +2,14 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useVibrate } from '../..'; describe('useVibrate', () => { - it('should be defined', () => { - expect(useVibrate).toBeDefined(); - }); + it('should be defined', () => { + expect(useVibrate).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => { - useVibrate(true, 100); - }); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => { + useVibrate(true, 100); + }); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useVibrate/index.ts b/src/useVibrate/index.ts index e58826ed8..1bda945f5 100644 --- a/src/useVibrate/index.ts +++ b/src/useVibrate/index.ts @@ -9,31 +9,31 @@ import { isBrowser, noop } from '../util/const'; * @param loop If true - vibration will be looped using `setInterval`. */ export const useVibrate = - !isBrowser || navigator.vibrate === undefined - ? noop - : (enabled: boolean, pattern: VibratePattern, loop?: boolean): void => { - useEffect(() => { - let interval: undefined | ReturnType; + !isBrowser || navigator.vibrate === undefined + ? noop + : (enabled: boolean, pattern: VibratePattern, loop?: boolean): void => { + useEffect(() => { + let interval: undefined | ReturnType; - if (enabled) { - navigator.vibrate(pattern); + if (enabled) { + navigator.vibrate(pattern); - if (loop) { - interval = setInterval( - () => { - navigator.vibrate(pattern); - }, - Array.isArray(pattern) ? pattern.reduce((a, n) => a + n, 0) : pattern - ); - } + if (loop) { + interval = setInterval( + () => { + navigator.vibrate(pattern); + }, + Array.isArray(pattern) ? pattern.reduce((a, n) => a + n, 0) : pattern + ); + } - return () => { - navigator.vibrate(0); + return () => { + navigator.vibrate(0); - if (interval) { - clearInterval(interval); - } - }; - } - }, [loop, pattern, enabled]); - }; + if (interval) { + clearInterval(interval); + } + }; + } + }, [loop, pattern, enabled]); + }; diff --git a/src/useWindowSize/__docs__/example.stories.tsx b/src/useWindowSize/__docs__/example.stories.tsx index 828f28fca..9edb8898e 100644 --- a/src/useWindowSize/__docs__/example.stories.tsx +++ b/src/useWindowSize/__docs__/example.stories.tsx @@ -2,15 +2,15 @@ import * as React from 'react'; import { useWindowSize } from '../..'; export function Example() { - const size = useWindowSize(); + const size = useWindowSize(); - return ( -
- Window dimensions: -
{JSON.stringify(size, null, 2)}
-
- Note: this example is rendered within an iframe which is smaller than your browser window. -
-
- ); + return ( +
+ Window dimensions: +
{JSON.stringify(size, null, 2)}
+
+ Note: this example is rendered within an iframe which is smaller than your browser window. +
+
+ ); } diff --git a/src/useWindowSize/__docs__/story.mdx b/src/useWindowSize/__docs__/story.mdx index 47deee6b5..aeb969ba3 100644 --- a/src/useWindowSize/__docs__/story.mdx +++ b/src/useWindowSize/__docs__/story.mdx @@ -15,15 +15,15 @@ Tracks inner dimensions of the browser window. #### Example - + ## Reference ```ts export interface WindowSize { - width: number; - height: number; + width: number; + height: number; } export function useWindowSize(stateHook = useRafState, measureOnMount?: boolean): WindowSize; diff --git a/src/useWindowSize/__tests__/dom.ts b/src/useWindowSize/__tests__/dom.ts index edf0ecee2..1048f2858 100644 --- a/src/useWindowSize/__tests__/dom.ts +++ b/src/useWindowSize/__tests__/dom.ts @@ -3,57 +3,57 @@ import { useState } from 'react'; import { useWindowSize, type WindowSize } from '../..'; describe('useWindowSize', () => { - beforeEach(() => { - window.innerWidth = 100; - window.innerHeight = 100; - }); - - const triggerResize = (dimension: 'width' | 'height', value: number) => { - if (dimension === 'width') { - window.innerWidth = value; - } else if (dimension === 'height') { - window.innerHeight = value; - } - - act(() => { - window.dispatchEvent(new Event('resize')); - }); - }; - - it('should be defined', () => { - expect(useWindowSize).toBeDefined(); - }); - - it('should render', () => { - const { result } = renderHook(() => useWindowSize()); - expect(result.error).toBeUndefined(); - }); - - it('should use provided state hook', () => { - const { result } = renderHook(() => useWindowSize(useState)); - - expect(result.current.width).toBe(100); - expect(result.current.height).toBe(100); - expect(result.all.length).toBe(1); - - triggerResize('width', 200); - expect(result.current.width).toBe(200); - expect(result.current.height).toBe(100); - expect(result.all.length).toBe(2); - - triggerResize('height', 200); - expect(result.current.width).toBe(200); - expect(result.current.height).toBe(200); - expect(result.all.length).toBe(3); - }); - - it('should delay measurement to effects stage if 2nd argument is `true`', () => { - const { result } = renderHook(() => useWindowSize(useState, true)); - - expect((result.all[0] as WindowSize).width).toBe(0); - expect((result.all[0] as WindowSize).height).toBe(0); - - expect(result.current.width).toBe(100); - expect(result.current.height).toBe(100); - }); + beforeEach(() => { + window.innerWidth = 100; + window.innerHeight = 100; + }); + + const triggerResize = (dimension: 'width' | 'height', value: number) => { + if (dimension === 'width') { + window.innerWidth = value; + } else if (dimension === 'height') { + window.innerHeight = value; + } + + act(() => { + window.dispatchEvent(new Event('resize')); + }); + }; + + it('should be defined', () => { + expect(useWindowSize).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => useWindowSize()); + expect(result.error).toBeUndefined(); + }); + + it('should use provided state hook', () => { + const { result } = renderHook(() => useWindowSize(useState)); + + expect(result.current.width).toBe(100); + expect(result.current.height).toBe(100); + expect(result.all.length).toBe(1); + + triggerResize('width', 200); + expect(result.current.width).toBe(200); + expect(result.current.height).toBe(100); + expect(result.all.length).toBe(2); + + triggerResize('height', 200); + expect(result.current.width).toBe(200); + expect(result.current.height).toBe(200); + expect(result.all.length).toBe(3); + }); + + it('should delay measurement to effects stage if 2nd argument is `true`', () => { + const { result } = renderHook(() => useWindowSize(useState, true)); + + expect((result.all[0] as WindowSize).width).toBe(0); + expect((result.all[0] as WindowSize).height).toBe(0); + + expect(result.current.width).toBe(100); + expect(result.current.height).toBe(100); + }); }); diff --git a/src/useWindowSize/__tests__/ssr.ts b/src/useWindowSize/__tests__/ssr.ts index 350bc8a51..5384ff228 100644 --- a/src/useWindowSize/__tests__/ssr.ts +++ b/src/useWindowSize/__tests__/ssr.ts @@ -2,12 +2,12 @@ import { renderHook } from '@testing-library/react-hooks/server'; import { useWindowSize } from '../..'; describe('useWindowSize', () => { - it('should be defined', () => { - expect(useWindowSize).toBeDefined(); - }); + it('should be defined', () => { + expect(useWindowSize).toBeDefined(); + }); - it('should render', () => { - const { result } = renderHook(() => useWindowSize()); - expect(result.error).toBeUndefined(); - }); + it('should render', () => { + const { result } = renderHook(() => useWindowSize()); + expect(result.error).toBeUndefined(); + }); }); diff --git a/src/useWindowSize/index.ts b/src/useWindowSize/index.ts index c43773438..f99c093f0 100644 --- a/src/useWindowSize/index.ts +++ b/src/useWindowSize/index.ts @@ -5,19 +5,19 @@ import { useRafState } from '../useRafState'; import { isBrowser } from '../util/const'; export type WindowSize = { - width: number; - height: number; + width: number; + height: number; }; const listeners = new Set<(size: WindowSize) => void>(); const callAllListeners = () => { - listeners.forEach((l) => { - l({ - width: window.innerWidth, - height: window.innerHeight, - }); - }); + listeners.forEach((l) => { + l({ + width: window.innerWidth, + height: window.innerHeight, + }); + }); }; /** @@ -29,36 +29,36 @@ const callAllListeners = () => { the component render. Set this to `true` during SSR. */ export function useWindowSize(stateHook = useRafState, measureOnMount?: boolean): WindowSize { - const isFirstMount = useFirstMountState(); - const [size, setSize] = stateHook({ - width: isFirstMount && isBrowser && !measureOnMount ? window.innerWidth : 0, - height: isFirstMount && isBrowser && !measureOnMount ? window.innerHeight : 0, - }); + const isFirstMount = useFirstMountState(); + const [size, setSize] = stateHook({ + width: isFirstMount && isBrowser && !measureOnMount ? window.innerWidth : 0, + height: isFirstMount && isBrowser && !measureOnMount ? window.innerHeight : 0, + }); - useMountEffect(() => { - if (measureOnMount) { - setSize({ - width: window.innerWidth, - height: window.innerHeight, - }); - } - }); + useMountEffect(() => { + if (measureOnMount) { + setSize({ + width: window.innerWidth, + height: window.innerHeight, + }); + } + }); - useEffect(() => { - if (listeners.size === 0) { - window.addEventListener('resize', callAllListeners, { passive: true }); - } + useEffect(() => { + if (listeners.size === 0) { + window.addEventListener('resize', callAllListeners, { passive: true }); + } - listeners.add(setSize); + listeners.add(setSize); - return () => { - listeners.delete(setSize); + return () => { + listeners.delete(setSize); - if (listeners.size === 0) { - window.removeEventListener('resize', callAllListeners); - } - }; - }, [setSize]); + if (listeners.size === 0) { + window.removeEventListener('resize', callAllListeners); + } + }; + }, [setSize]); - return size; + return size; } diff --git a/src/util/__tests__/dom.ts b/src/util/__tests__/dom.ts index 014ca6995..6b20d68cf 100644 --- a/src/util/__tests__/dom.ts +++ b/src/util/__tests__/dom.ts @@ -2,85 +2,85 @@ import { resolveHookState } from '../..'; import { basicDepsComparator, off, on } from '../misc'; describe('resolveHookState', () => { - it('should be defined', () => { - expect(resolveHookState).toBeDefined(); - }); + it('should be defined', () => { + expect(resolveHookState).toBeDefined(); + }); - it('should return value itself if it is not function', () => { - expect(resolveHookState(123)).toBe(123); + it('should return value itself if it is not function', () => { + expect(resolveHookState(123)).toBe(123); - const obj = { foo: 'bar' }; - expect(resolveHookState(obj)).toBe(obj); - }); + const obj = { foo: 'bar' }; + expect(resolveHookState(obj)).toBe(obj); + }); - it('should return call result in case function received', () => { - expect(resolveHookState(() => 123)).toBe(123); + it('should return call result in case function received', () => { + expect(resolveHookState(() => 123)).toBe(123); - const obj = { foo: 'bar' }; - expect(resolveHookState(() => obj)).toBe(obj); - }); + const obj = { foo: 'bar' }; + expect(resolveHookState(() => obj)).toBe(obj); + }); - it('should pass second parameter to received function', () => { - expect(resolveHookState((state) => state, 123)).toBe(123); + it('should pass second parameter to received function', () => { + expect(resolveHookState((state) => state, 123)).toBe(123); - const obj = { foo: 'bar' }; - expect(resolveHookState((state) => state, obj)).toBe(obj); - }); + const obj = { foo: 'bar' }; + expect(resolveHookState((state) => state, obj)).toBe(obj); + }); }); const cb = () => {}; describe('misc', () => { - describe('on', () => { - it("should call object's `addEventListener` with passed parameters", () => { - const obj = { - addEventListener: jest.fn(), - }; - on(obj as unknown as EventTarget, 'evtName', cb); - expect(obj.addEventListener).toHaveBeenCalledWith('evtName', cb); - }); - it("should not throw in case 'undefined' element passed", () => { - expect(() => { - // @ts-expect-error testing inappropriate usage - on(undefined, 'evtName', () => {}); - }).not.toThrow(); - }); - }); - - describe('off', () => { - it("should call object's `removeEventListener` with passed parameters", () => { - const obj = { - removeEventListener: jest.fn(), - }; - - off(obj as unknown as EventTarget, 'evtName', cb); - expect(obj.removeEventListener).toHaveBeenCalledWith('evtName', cb); - }); - - it("should not throw in case 'undefined' element passed", () => { - expect(() => { - // @ts-expect-error testing inappropriate usage - off(undefined, 'evtName', () => {}); - }).not.toThrow(); - }); - }); - - describe('basicDepsComparator', () => { - it('should return true if both arrays ref-equal', () => { - const d1 = [1, 2, 3]; - expect(basicDepsComparator(d1, d1)).toBe(true); - }); - - it('should return false in case array has different length', () => { - expect(basicDepsComparator([1], [1, 2])).toBe(false); - }); - - it('should return false in respective elements not equal', () => { - expect(basicDepsComparator([1, 2, 3], [1, 3, 2])).toBe(false); - }); - - it('should return true in case arrays are equal', () => { - expect(basicDepsComparator([1, 2, 3], [1, 2, 3])).toBe(true); - }); - }); + describe('on', () => { + it("should call object's `addEventListener` with passed parameters", () => { + const obj = { + addEventListener: jest.fn(), + }; + on(obj as unknown as EventTarget, 'evtName', cb); + expect(obj.addEventListener).toHaveBeenCalledWith('evtName', cb); + }); + it("should not throw in case 'undefined' element passed", () => { + expect(() => { + // @ts-expect-error testing inappropriate usage + on(undefined, 'evtName', () => {}); + }).not.toThrow(); + }); + }); + + describe('off', () => { + it("should call object's `removeEventListener` with passed parameters", () => { + const obj = { + removeEventListener: jest.fn(), + }; + + off(obj as unknown as EventTarget, 'evtName', cb); + expect(obj.removeEventListener).toHaveBeenCalledWith('evtName', cb); + }); + + it("should not throw in case 'undefined' element passed", () => { + expect(() => { + // @ts-expect-error testing inappropriate usage + off(undefined, 'evtName', () => {}); + }).not.toThrow(); + }); + }); + + describe('basicDepsComparator', () => { + it('should return true if both arrays ref-equal', () => { + const d1 = [1, 2, 3]; + expect(basicDepsComparator(d1, d1)).toBe(true); + }); + + it('should return false in case array has different length', () => { + expect(basicDepsComparator([1], [1, 2])).toBe(false); + }); + + it('should return false in respective elements not equal', () => { + expect(basicDepsComparator([1, 2, 3], [1, 3, 2])).toBe(false); + }); + + it('should return true in case arrays are equal', () => { + expect(basicDepsComparator([1, 2, 3], [1, 2, 3])).toBe(true); + }); + }); }); diff --git a/src/util/const.ts b/src/util/const.ts index 0f7e53310..8dd47449c 100644 --- a/src/util/const.ts +++ b/src/util/const.ts @@ -3,9 +3,9 @@ import type { Predicate, ConditionsPredicate } from '../types'; export const noop = (): void => {}; export const isBrowser = - typeof window !== 'undefined' && - typeof navigator !== 'undefined' && - typeof document !== 'undefined'; + typeof window !== 'undefined' && + typeof navigator !== 'undefined' && + typeof document !== 'undefined'; /** * You should only be reaching for this function when you're attempting to prevent multiple @@ -15,7 +15,7 @@ export const isBrowser = export const isStrictEqual: Predicate = (prev: any, next: any): boolean => prev === next; export const truthyAndArrayPredicate: ConditionsPredicate = (conditions): boolean => - conditions.every(Boolean); + conditions.every(Boolean); export const truthyOrArrayPredicate: ConditionsPredicate = (conditions): boolean => - conditions.some(Boolean); + conditions.some(Boolean); diff --git a/src/util/misc.ts b/src/util/misc.ts index 434a26efc..2c0715956 100644 --- a/src/util/misc.ts +++ b/src/util/misc.ts @@ -2,52 +2,52 @@ import { type DependencyList } from 'react'; import type { DependenciesComparator } from '../types'; export function on( - obj: T | null, - ...args: - | Parameters - | [string, EventListenerOrEventListenerObject | CallableFunction, ...any] + obj: T | null, + ...args: + | Parameters + | [string, EventListenerOrEventListenerObject | CallableFunction, ...any] ): void { - obj?.addEventListener?.(...(args as Parameters)); + obj?.addEventListener?.(...(args as Parameters)); } export function off( - obj: T | null, - ...args: - | Parameters - | [string, EventListenerOrEventListenerObject | CallableFunction, ...any] + obj: T | null, + ...args: + | Parameters + | [string, EventListenerOrEventListenerObject | CallableFunction, ...any] ): void { - obj?.removeEventListener?.(...(args as Parameters)); + obj?.removeEventListener?.(...(args as Parameters)); } export const hasOwnProperty = < - T extends Record, - K extends string | number | symbol + T extends Record, + K extends string | number | symbol >( - obj: T, - property: K + obj: T, + property: K ): obj is T & Record => Object.prototype.hasOwnProperty.call(obj, property); export const yieldTrue = () => true as const; export const yieldFalse = () => false as const; export const basicDepsComparator: DependenciesComparator = (d1, d2) => { - if (d1 === d2) return true; + if (d1 === d2) return true; - if (d1.length !== d2.length) return false; + if (d1.length !== d2.length) return false; - for (let i = 0; i < d1.length; i++) { - if (d1[i] !== d2[i]) { - return false; - } - } + for (let i = 0; i < d1.length; i++) { + if (d1[i] !== d2[i]) { + return false; + } + } - return true; + return true; }; export type EffectCallback = (...args: any[]) => any; export type EffectHook< - Callback extends EffectCallback = EffectCallback, - Deps extends DependencyList | undefined = DependencyList | undefined, - RestArgs extends any[] = any[] + Callback extends EffectCallback = EffectCallback, + Deps extends DependencyList | undefined = DependencyList | undefined, + RestArgs extends any[] = any[] > = ((...args: [Callback, Deps, ...RestArgs]) => void) | ((...args: [Callback, Deps]) => void); diff --git a/src/util/resolveHookState.ts b/src/util/resolveHookState.ts index 6c506de1f..1f5450c15 100644 --- a/src/util/resolveHookState.ts +++ b/src/util/resolveHookState.ts @@ -3,15 +3,15 @@ export type NextState = State | ((prevState: PrevState export function resolveHookState(nextState: InitialState): State; export function resolveHookState( - nextState: NextState, - prevState: PrevState + nextState: NextState, + prevState: PrevState ): State; export function resolveHookState( - nextState: InitialState | NextState, - prevState?: PrevState + nextState: InitialState | NextState, + prevState?: PrevState ): State { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - if (typeof nextState === 'function') return (nextState as CallableFunction)(prevState); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + if (typeof nextState === 'function') return (nextState as CallableFunction)(prevState); - return nextState; + return nextState; } diff --git a/utility/add-new-hook.js b/utility/add-new-hook.js index fca820cf4..a065b3dfb 100644 --- a/utility/add-new-hook.js +++ b/utility/add-new-hook.js @@ -3,46 +3,46 @@ const fs = require('fs/promises'); // eslint-disable-next-line no-void void (async () => { - const scriptPath = process.argv[1].trim(); - const hookName = process.argv[2]?.trim(); - - if (!hookName || hookName.length === 0) { - throw new TypeError('hook name not defined'); - } - - const srcDir = path.resolve(scriptPath, '../../src'); - const hookDir = path.join(srcDir, hookName); - - if ( - await fs - .lstat(hookDir) - .then(() => true) - .catch(() => false) - ) { - throw new TypeError(`directory for hook ${hookName} already exists`); - } - - await fs.appendFile(path.join(srcDir, 'index.ts'), `\nexport * from './${hookName}';\n`); - - await fs.mkdir(hookDir); - await fs.writeFile( - path.resolve(hookDir, `index.ts`), - `export function ${hookName}(): void {\n}\n` - ); - - await fs.mkdir(path.resolve(hookDir, `__docs__`)); - await fs.writeFile( - path.resolve(hookDir, `__docs__/example.stories.tsx`), - `import * as React from 'react'; + const scriptPath = process.argv[1].trim(); + const hookName = process.argv[2]?.trim(); + + if (!hookName || hookName.length === 0) { + throw new TypeError('hook name not defined'); + } + + const srcDir = path.resolve(scriptPath, '../../src'); + const hookDir = path.join(srcDir, hookName); + + if ( + await fs + .lstat(hookDir) + .then(() => true) + .catch(() => false) + ) { + throw new TypeError(`directory for hook ${hookName} already exists`); + } + + await fs.appendFile(path.join(srcDir, 'index.ts'), `\nexport * from './${hookName}';\n`); + + await fs.mkdir(hookDir); + await fs.writeFile( + path.resolve(hookDir, `index.ts`), + `export function ${hookName}(): void {\n}\n` + ); + + await fs.mkdir(path.resolve(hookDir, `__docs__`)); + await fs.writeFile( + path.resolve(hookDir, `__docs__/example.stories.tsx`), + `import * as React from 'react'; import { ${hookName} } from '../..'; export const Example: React.FC = () => { return null; }` - ); - await fs.writeFile( - path.resolve(hookDir, `__docs__/story.mdx`), - `import { Canvas, Meta, Story } from '@storybook/addon-docs'; + ); + await fs.writeFile( + path.resolve(hookDir, `__docs__/story.mdx`), + `import { Canvas, Meta, Story } from '@storybook/addon-docs'; import { Example } from './example.stories'; import { ImportPath } from '../../__docs__/ImportPath'; @@ -70,12 +70,12 @@ import { ImportPath } from '../../__docs__/ImportPath'; #### Return ` - ); + ); - await fs.mkdir(path.resolve(hookDir, `__tests__`)); - await fs.writeFile( - path.resolve(hookDir, `__tests__/dom.ts`), - `import { renderHook } from '@testing-library/react-hooks/dom'; + await fs.mkdir(path.resolve(hookDir, `__tests__`)); + await fs.writeFile( + path.resolve(hookDir, `__tests__/dom.ts`), + `import { renderHook } from '@testing-library/react-hooks/dom'; import { ${hookName} } from '../..'; describe('${hookName}', () => { @@ -89,10 +89,10 @@ describe('${hookName}', () => { }); }); ` - ); - await fs.writeFile( - path.resolve(hookDir, `__tests__/ssr.ts`), - `import { renderHook } from '@testing-library/react-hooks/server'; + ); + await fs.writeFile( + path.resolve(hookDir, `__tests__/ssr.ts`), + `import { renderHook } from '@testing-library/react-hooks/server'; import { ${hookName} } from '../..'; describe('${hookName}', () => { @@ -106,5 +106,5 @@ describe('${hookName}', () => { }); }); ` - ); + ); })(); diff --git a/utility/ts-transformer-js-extension.ts b/utility/ts-transformer-js-extension.ts index 0fb0a5030..d00f1999a 100644 --- a/utility/ts-transformer-js-extension.ts +++ b/utility/ts-transformer-js-extension.ts @@ -3,77 +3,77 @@ import * as path from 'node:path'; import * as ts from 'typescript'; function shouldUpdateImportDeclaration( - node: ts.Node + node: ts.Node ): node is (ts.ImportDeclaration | ts.ExportDeclaration) & { moduleSpecifier: ts.StringLiteral } { - if (!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node)) { - return false; - } + if (!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node)) { + return false; + } - if (node.moduleSpecifier === undefined) { - return false; - } + if (node.moduleSpecifier === undefined) { + return false; + } - if (!ts.isStringLiteral(node.moduleSpecifier)) { - return false; - } + if (!ts.isStringLiteral(node.moduleSpecifier)) { + return false; + } - if (!node.moduleSpecifier.text.startsWith('./') && !node.moduleSpecifier.text.startsWith('../')) { - return false; - } + if (!node.moduleSpecifier.text.startsWith('./') && !node.moduleSpecifier.text.startsWith('../')) { + return false; + } - return path.extname(node.moduleSpecifier.text) === ''; + return path.extname(node.moduleSpecifier.text) === ''; } /** * Checks whether provided node imports file or directory. */ function isDirectoryImport( - node: ts.Node & { moduleSpecifier: ts.StringLiteral; original?: ts.Node } + node: ts.Node & { moduleSpecifier: ts.StringLiteral; original?: ts.Node } ): boolean { - const importPath = path.resolve( - path.dirname((node.original ?? node).getSourceFile().fileName), - node.moduleSpecifier.text - ); + const importPath = path.resolve( + path.dirname((node.original ?? node).getSourceFile().fileName), + node.moduleSpecifier.text + ); - try { - return fs.statSync(importPath).isDirectory(); - } catch { - return false; - } + try { + return fs.statSync(importPath).isDirectory(); + } catch { + return false; + } } export default function transformer(_: ts.Program): ts.TransformerFactory { - return (context) => (sourceFile) => { - const fac = context.factory; - const visitor = (node: ts.Node): ts.VisitResult => { - if (shouldUpdateImportDeclaration(node) && !isDirectoryImport(node)) { - if (ts.isImportDeclaration(node)) { - const newModuleSpecifier = fac.createStringLiteral(`${node.moduleSpecifier.text}.js`); - return fac.updateImportDeclaration( - node, - node.modifiers, - node.importClause, - newModuleSpecifier, - node.assertClause - ); - } + return (context) => (sourceFile) => { + const fac = context.factory; + const visitor = (node: ts.Node): ts.VisitResult => { + if (shouldUpdateImportDeclaration(node) && !isDirectoryImport(node)) { + if (ts.isImportDeclaration(node)) { + const newModuleSpecifier = fac.createStringLiteral(`${node.moduleSpecifier.text}.js`); + return fac.updateImportDeclaration( + node, + node.modifiers, + node.importClause, + newModuleSpecifier, + node.assertClause + ); + } - if (ts.isExportDeclaration(node)) { - const newModuleSpecifier = fac.createStringLiteral(`${node.moduleSpecifier.text}.js`); - return fac.updateExportDeclaration( - node, - node.modifiers, - false, - node.exportClause, - newModuleSpecifier, - node.assertClause - ); - } - } + if (ts.isExportDeclaration(node)) { + const newModuleSpecifier = fac.createStringLiteral(`${node.moduleSpecifier.text}.js`); + return fac.updateExportDeclaration( + node, + node.modifiers, + false, + node.exportClause, + newModuleSpecifier, + node.assertClause + ); + } + } - return ts.visitEachChild(node, visitor, context); - }; + return ts.visitEachChild(node, visitor, context); + }; - return ts.visitNode(sourceFile, visitor); - }; + return ts.visitNode(sourceFile, visitor); + }; }