├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc.yaml ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── useEvent.test-react-17.ts ├── useEvent.test.ts └── useEvent.ts ├── test └── react-17 │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ └── useEvent.react-17.test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | max_line_length = 120 3 | indent_style = space 4 | indent_size = 2 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | quote_type = double 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - '**' 9 | - '!README.md' 10 | pull_request: 11 | paths: 12 | - '**' 13 | - '!README.md' 14 | 15 | jobs: 16 | test: 17 | name: "${{ matrix.command }}" 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | command: 22 | - npm run test:react-18 23 | - npm run test:react-17 24 | - npm run lint 25 | - npm run typecheck 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: 18.x 31 | cache: 'npm' 32 | 33 | - name: Installation 34 | run: npm ci 35 | 36 | - name: "${{ matrix.command }}" 37 | run: ${{ matrix.command }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated folders 2 | node_modules 3 | .idea 4 | dist 5 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Scott Rippey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-use-event-hook 2 | Same as React's `useCallback`, but returns a stable reference. 3 | 4 | This library is a user-land implementation of the `useEvent` hook, [proposed in this RFC](https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md). 5 | 6 | # Installation 7 | 8 | ```sh 9 | npm install react-use-event-hook 10 | ``` 11 | 12 | # Usage 13 | (this example was copied from the RFC) 14 | 15 | You can wrap any event handler into `useEvent`. 16 | 17 | ```js 18 | import useEvent from 'react-use-event-hook'; 19 | 20 | function Chat() { 21 | const [text, setText] = useState(''); 22 | 23 | const onClick = useEvent(() => { 24 | sendMessage(text); 25 | }); 26 | 27 | return ; 28 | } 29 | ``` 30 | 31 | The code inside `useEvent` “sees” the props/state values at the time of the call. 32 | The returned function has a stable identity even if the props/state it references change. 33 | There is no dependency array. 34 | 35 | # See more 36 | - [The proposed `useEvent` RFC](https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md) 37 | - [A hearty discussion on the naming, and edge-case considerations, of this hook](https://github.com/reactjs/rfcs/pull/220) 38 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/w1/390hkp6x0g1dg5z5l50651pc0000gn/T/jest_dx", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | // clearMocks: false, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | // coverageDirectory: undefined, 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | // coverageProvider: "babel", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "json", 77 | // "jsx", 78 | // "ts", 79 | // "tsx", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | preset: "ts-jest", 97 | 98 | // Run tests from one or more projects 99 | // projects: undefined, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state between every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: undefined, 112 | 113 | // Automatically restore mock state between every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | rootDir: "src", 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | // roots: [ 121 | // "" 122 | // ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | // setupFiles: [], 129 | 130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 131 | // setupFilesAfterEnv: [], 132 | 133 | // The number of seconds after which a test is considered as slow and reported as such in the results. 134 | // slowTestThreshold: 5, 135 | 136 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 137 | // snapshotSerializers: [], 138 | 139 | // The test environment that will be used for testing 140 | // testEnvironment: "jest-environment-jsdom", 141 | 142 | // Options that will be passed to the testEnvironment 143 | // testEnvironmentOptions: {}, 144 | 145 | // Adds a location field to test results 146 | // testLocationInResults: false, 147 | 148 | // The glob patterns Jest uses to detect test files 149 | // testMatch: [ 150 | // "**/__tests__/**/*.[jt]s?(x)", 151 | // "**/?(*.)+(spec|test).[tj]s?(x)" 152 | // ], 153 | 154 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 155 | // testPathIgnorePatterns: [ 156 | // "/node_modules/" 157 | // ], 158 | 159 | // The regexp pattern or array of patterns that Jest uses to detect test files 160 | // testRegex: [], 161 | 162 | // This option allows the use of a custom results processor 163 | // testResultsProcessor: undefined, 164 | 165 | // This option allows use of a custom test runner 166 | // testRunner: "jasmine2", 167 | 168 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 169 | // testURL: "http://localhost", 170 | 171 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 172 | // timers: "real", 173 | 174 | // A map from regular expressions to paths to transformers 175 | // transform: undefined, 176 | 177 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 178 | // transformIgnorePatterns: [ 179 | // "/node_modules/", 180 | // "\\.pnp\\.[^\\/]+$" 181 | // ], 182 | 183 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 184 | // unmockedModulePathPatterns: undefined, 185 | 186 | // Indicates whether each individual test should be reported during the run 187 | // verbose: undefined, 188 | 189 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 190 | // watchPathIgnorePatterns: [], 191 | 192 | // Whether to use watchman for file crawling 193 | // watchman: true, 194 | }; 195 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-use-event-hook", 3 | "version": "0.9.5", 4 | "description": "Same as React's `useCallback`, but returns a stable reference.", 5 | "main": "dist/cjs/useEvent.js", 6 | "module": "dist/esm/useEvent.js", 7 | "types": "dist/esm/useEvent.d.ts", 8 | "files": [ 9 | "dist", 10 | "src" 11 | ], 12 | "scripts": { 13 | "check": "npm run lint && npm run typecheck && npm run test:react-18 && npm run test:react-17", 14 | "test": "jest", 15 | "test:react-18": "jest", 16 | "test:react-17": "cd test/react-17; test -d node_modules || npm ci; npm run test", 17 | "test:watch": "jest --watch", 18 | "lint": "prettier src --check", 19 | "lint:fix": "prettier src --write", 20 | "build": "npm run clean && npm run build:esm && npm run build:cjs", 21 | "build:esm": "tsc", 22 | "build:cjs": "tsc --outDir dist/cjs --module commonjs", 23 | "clean": "rimraf dist", 24 | "build:watch": "tsc --watch", 25 | "typecheck": "tsc --noEmit", 26 | "prepublishOnly": "npm run build", 27 | "preversion": "npm run test && npm run typecheck" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/scottrippey/react-use-event-hook.git" 32 | }, 33 | "keywords": [ 34 | "react", 35 | "hooks", 36 | "useevent", 37 | "memo", 38 | "performance", 39 | "optimization" 40 | ], 41 | "author": "Scott Rippey", 42 | "license": "MIT", 43 | "bugs": { 44 | "url": "https://github.com/scottrippey/react-use-event-hook/issues" 45 | }, 46 | "homepage": "https://github.com/scottrippey/react-use-event-hook#readme", 47 | "dependencies": {}, 48 | "peerDependencies": { 49 | "react": ">=16.8.0" 50 | }, 51 | "devDependencies": { 52 | "@testing-library/react": "^13.4.0", 53 | "@types/jest": "^26.0.20", 54 | "jest": "^26.6.3", 55 | "prettier": "^2.2.1", 56 | "react": "^18.2.0", 57 | "rimraf": "^3.0.2", 58 | "ts-jest": "^26.4.4", 59 | "typescript": "^4.1.3" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/useEvent.test-react-17.ts: -------------------------------------------------------------------------------- 1 | // Run all the other tests: 2 | import "./useEvent.test"; 3 | 4 | // Just to make sure our overrides are working: 5 | import React from "react"; 6 | it(`we're testing React 17`, async () => { 7 | expect(React.version).toMatchInlineSnapshot(`"17.0.2"`); 8 | }); 9 | -------------------------------------------------------------------------------- /src/useEvent.test.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { renderHook } from "@testing-library/react"; 3 | import { useEvent } from "./useEvent"; 4 | 5 | // Only available in React 18+ 6 | const reactSupportsUseInsertionEffect = !!React.useInsertionEffect; 7 | 8 | describe(`useEvent (React ${React.version})`, () => { 9 | let initialCallback = jest.fn((...args) => args); 10 | let stableCallback: jest.Mock; 11 | let rerender: (newCallback?: jest.Mock) => void; 12 | 13 | function renderTestHook() { 14 | const result = renderHook( 15 | (latestCallback) => { 16 | stableCallback = useEvent(latestCallback); 17 | }, 18 | { initialProps: initialCallback } 19 | ); 20 | rerender = result.rerender; 21 | } 22 | 23 | beforeEach(() => { 24 | jest.clearAllMocks(); 25 | renderTestHook(); 26 | }); 27 | 28 | it("should return a different function", () => { 29 | expect(typeof stableCallback).toEqual("function"); 30 | expect(stableCallback).not.toBe(initialCallback); 31 | expect(initialCallback).not.toHaveBeenCalled(); 32 | }); 33 | 34 | it("calling the stableCallback should call the initialCallback", () => { 35 | stableCallback(); 36 | expect(initialCallback).toHaveBeenCalled(); 37 | }); 38 | 39 | it("all params and return value should be passed through", () => { 40 | const returnValue = stableCallback(1, 2, 3); 41 | expect(initialCallback).toHaveBeenCalledWith(1, 2, 3); 42 | expect(returnValue).toEqual([1, 2, 3]); 43 | }); 44 | 45 | it('will pass through the current "this" value', () => { 46 | const thisObj = { stableCallback }; 47 | thisObj.stableCallback(1, 2, 3); 48 | expect(initialCallback).toHaveBeenCalledTimes(1); 49 | expect(initialCallback.mock.instances[0]).toBe(thisObj); 50 | }); 51 | 52 | describe("timing", () => { 53 | beforeEach(() => { 54 | jest.spyOn(console, "error").mockImplementation(() => { 55 | /* suppress Reacts error logging */ 56 | }); 57 | }); 58 | afterEach(() => { 59 | jest.restoreAllMocks(); 60 | }); 61 | 62 | it("will throw an error if called during render", () => { 63 | const useEventBeforeMount = () => { 64 | const cb = useEvent(() => 5); 65 | cb(); 66 | }; 67 | expect(() => { 68 | const r = renderHook(() => useEventBeforeMount()); 69 | 70 | // @ts-expect-error This is just for React 17: 71 | if (r.result.error) throw r.result.error; 72 | }).toThrowErrorMatchingInlineSnapshot( 73 | `"INVALID_USEEVENT_INVOCATION: the callback from useEvent cannot be invoked before the component has mounted."` 74 | ); 75 | }); 76 | 77 | it("will work fine if called inside a useLayoutEffect", () => { 78 | const useEventInLayoutEffect = () => { 79 | const [state, setState] = React.useState(0); 80 | const cb = useEvent(() => 5); 81 | React.useLayoutEffect(() => { 82 | setState(cb()); 83 | }, []); 84 | return state; 85 | }; 86 | const { result } = renderHook(() => useEventInLayoutEffect()); 87 | expect(result).toMatchObject({ current: 5 }); 88 | }); 89 | 90 | describe("when used in a NESTED useLayoutEffect", () => { 91 | const renderNestedTest = () => { 92 | /** 93 | * This is a tricky edge-case scenario that happens in React 16/17. 94 | * 95 | * We update our callback inside a `useLayoutEffect`. 96 | * With nested React components, `useLayoutEffect` gets called 97 | * in children first, parents last. 98 | * 99 | * So if we pass a `useEvent` callback into a child component, 100 | * and the child component calls it in a useLayoutEffect, 101 | * we will throw an error. 102 | */ 103 | 104 | // Since we're testing this with react-hooks, we need to use a Context to achieve parent-child hierarchy 105 | const ctx = React.createContext<{ callback(): number }>(null!); 106 | const wrapper: React.FC = (props) => { 107 | const callback = useEvent(() => 5); 108 | return React.createElement(ctx.Provider, { value: { callback } }, props.children); 109 | }; 110 | 111 | const { result } = renderHook( 112 | () => { 113 | const [layoutResult, setLayoutResult] = React.useState(null); 114 | const { callback } = React.useContext(ctx); 115 | React.useLayoutEffect(() => { 116 | // Unfortunately, renderHook won't capture a layout error. 117 | // Instead, we'll manually capture it: 118 | try { 119 | setLayoutResult({ callbackResult: callback() }); 120 | } catch (err) { 121 | setLayoutResult({ layoutError: err }); 122 | } 123 | }, []); 124 | 125 | return layoutResult; 126 | }, 127 | { wrapper } 128 | ); 129 | 130 | return result; 131 | }; 132 | 133 | if (!reactSupportsUseInsertionEffect) { 134 | // React 17 135 | it("will throw an error", () => { 136 | const result = renderNestedTest(); 137 | expect(result.current).toMatchInlineSnapshot(` 138 | Object { 139 | "layoutError": [Error: INVALID_USEEVENT_INVOCATION: the callback from useEvent cannot be invoked before the component has mounted.], 140 | } 141 | `); 142 | }); 143 | } else { 144 | // React 18+ 145 | it("will have no problems because of useInjectionEffect", () => { 146 | const result = renderNestedTest(); 147 | expect(result.current).toMatchInlineSnapshot(` 148 | Object { 149 | "callbackResult": 5, 150 | } 151 | `); 152 | }); 153 | } 154 | }); 155 | }); 156 | 157 | describe("when the hook is rerendered", () => { 158 | let newCallback = jest.fn(); 159 | let originalStableCallback: typeof stableCallback; 160 | beforeEach(() => { 161 | originalStableCallback = stableCallback; 162 | rerender(newCallback); 163 | }); 164 | 165 | it("the stableCallback is stable", () => { 166 | expect(stableCallback).toBe(originalStableCallback); 167 | }); 168 | 169 | it("calling the stableCallback only calls the latest callback", () => { 170 | stableCallback(); 171 | expect(initialCallback).not.toHaveBeenCalled(); 172 | expect(newCallback).toHaveBeenCalled(); 173 | }); 174 | 175 | it("the same goes for the 3rd render, etc", () => { 176 | const thirdCallback = jest.fn(); 177 | rerender(thirdCallback); 178 | stableCallback(); 179 | expect(initialCallback).not.toHaveBeenCalled(); 180 | expect(newCallback).not.toHaveBeenCalled(); 181 | expect(thirdCallback).toHaveBeenCalled(); 182 | }); 183 | }); 184 | }); 185 | -------------------------------------------------------------------------------- /src/useEvent.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type AnyFunction = (...args: any[]) => any; 4 | 5 | /** 6 | * Suppress the warning when using useLayoutEffect with SSR. (https://reactjs.org/link/uselayouteffect-ssr) 7 | * Make use of useInsertionEffect if available. 8 | */ 9 | const useInsertionEffect = 10 | typeof window !== "undefined" 11 | ? // useInsertionEffect is available in React 18+ 12 | React.useInsertionEffect || React.useLayoutEffect 13 | : () => {}; 14 | 15 | /** 16 | * Similar to useCallback, with a few subtle differences: 17 | * - The returned function is a stable reference, and will always be the same between renders 18 | * - No dependency lists required 19 | * - Properties or state accessed within the callback will always be "current" 20 | */ 21 | export function useEvent(callback: TCallback): TCallback { 22 | // Keep track of the latest callback: 23 | const latestRef = React.useRef(useEvent_shouldNotBeInvokedBeforeMount as any); 24 | useInsertionEffect(() => { 25 | latestRef.current = callback; 26 | }, [callback]); 27 | 28 | // Create a stable callback that always calls the latest callback: 29 | // using useRef instead of useCallback avoids creating and empty array on every render 30 | const stableRef = React.useRef(null as any); 31 | if (!stableRef.current) { 32 | stableRef.current = function (this: any) { 33 | return latestRef.current.apply(this, arguments as any); 34 | } as TCallback; 35 | } 36 | 37 | return stableRef.current; 38 | } 39 | 40 | /** 41 | * Render methods should be pure, especially when concurrency is used, 42 | * so we will throw this error if the callback is called while rendering. 43 | */ 44 | function useEvent_shouldNotBeInvokedBeforeMount() { 45 | throw new Error( 46 | "INVALID_USEEVENT_INVOCATION: the callback from useEvent cannot be invoked before the component has mounted." 47 | ); 48 | } 49 | 50 | export default useEvent; 51 | -------------------------------------------------------------------------------- /test/react-17/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("../../jest.config"), 3 | rootDir: ".", 4 | moduleNameMapper: { 5 | // Ensure we "lock" the React version to v17 for these tests: 6 | "^react$": "/node_modules/react", 7 | // Use react-hooks instead (we have a tiny bit of interop code to ensure the tests still work) 8 | "^@testing-library/react$": "/node_modules/@testing-library/react-hooks", 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /test/react-17/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-17", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "devDependencies": { 8 | "@testing-library/react-hooks": "^8.0.1", 9 | "react": "^17.0.2", 10 | "react-test-renderer": "^17.0.2" 11 | } 12 | }, 13 | "node_modules/@babel/runtime": { 14 | "version": "7.19.0", 15 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.0.tgz", 16 | "integrity": "sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==", 17 | "dev": true, 18 | "dependencies": { 19 | "regenerator-runtime": "^0.13.4" 20 | }, 21 | "engines": { 22 | "node": ">=6.9.0" 23 | } 24 | }, 25 | "node_modules/@testing-library/react-hooks": { 26 | "version": "8.0.1", 27 | "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz", 28 | "integrity": "sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==", 29 | "dev": true, 30 | "dependencies": { 31 | "@babel/runtime": "^7.12.5", 32 | "react-error-boundary": "^3.1.0" 33 | }, 34 | "engines": { 35 | "node": ">=12" 36 | }, 37 | "peerDependencies": { 38 | "@types/react": "^16.9.0 || ^17.0.0", 39 | "react": "^16.9.0 || ^17.0.0", 40 | "react-dom": "^16.9.0 || ^17.0.0", 41 | "react-test-renderer": "^16.9.0 || ^17.0.0" 42 | }, 43 | "peerDependenciesMeta": { 44 | "@types/react": { 45 | "optional": true 46 | }, 47 | "react-dom": { 48 | "optional": true 49 | }, 50 | "react-test-renderer": { 51 | "optional": true 52 | } 53 | } 54 | }, 55 | "node_modules/js-tokens": { 56 | "version": "4.0.0", 57 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 58 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 59 | "dev": true 60 | }, 61 | "node_modules/loose-envify": { 62 | "version": "1.4.0", 63 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 64 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 65 | "dev": true, 66 | "dependencies": { 67 | "js-tokens": "^3.0.0 || ^4.0.0" 68 | }, 69 | "bin": { 70 | "loose-envify": "cli.js" 71 | } 72 | }, 73 | "node_modules/object-assign": { 74 | "version": "4.1.1", 75 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 76 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 77 | "dev": true, 78 | "engines": { 79 | "node": ">=0.10.0" 80 | } 81 | }, 82 | "node_modules/react": { 83 | "version": "17.0.2", 84 | "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", 85 | "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", 86 | "dev": true, 87 | "dependencies": { 88 | "loose-envify": "^1.1.0", 89 | "object-assign": "^4.1.1" 90 | }, 91 | "engines": { 92 | "node": ">=0.10.0" 93 | } 94 | }, 95 | "node_modules/react-error-boundary": { 96 | "version": "3.1.4", 97 | "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", 98 | "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", 99 | "dev": true, 100 | "dependencies": { 101 | "@babel/runtime": "^7.12.5" 102 | }, 103 | "engines": { 104 | "node": ">=10", 105 | "npm": ">=6" 106 | }, 107 | "peerDependencies": { 108 | "react": ">=16.13.1" 109 | } 110 | }, 111 | "node_modules/react-is": { 112 | "version": "17.0.2", 113 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", 114 | "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", 115 | "dev": true 116 | }, 117 | "node_modules/react-shallow-renderer": { 118 | "version": "16.15.0", 119 | "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", 120 | "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", 121 | "dev": true, 122 | "dependencies": { 123 | "object-assign": "^4.1.1", 124 | "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" 125 | }, 126 | "peerDependencies": { 127 | "react": "^16.0.0 || ^17.0.0 || ^18.0.0" 128 | } 129 | }, 130 | "node_modules/react-test-renderer": { 131 | "version": "17.0.2", 132 | "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz", 133 | "integrity": "sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ==", 134 | "dev": true, 135 | "dependencies": { 136 | "object-assign": "^4.1.1", 137 | "react-is": "^17.0.2", 138 | "react-shallow-renderer": "^16.13.1", 139 | "scheduler": "^0.20.2" 140 | }, 141 | "peerDependencies": { 142 | "react": "17.0.2" 143 | } 144 | }, 145 | "node_modules/regenerator-runtime": { 146 | "version": "0.13.9", 147 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", 148 | "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", 149 | "dev": true 150 | }, 151 | "node_modules/scheduler": { 152 | "version": "0.20.2", 153 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", 154 | "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", 155 | "dev": true, 156 | "dependencies": { 157 | "loose-envify": "^1.1.0", 158 | "object-assign": "^4.1.1" 159 | } 160 | } 161 | }, 162 | "dependencies": { 163 | "@babel/runtime": { 164 | "version": "7.19.0", 165 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.0.tgz", 166 | "integrity": "sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==", 167 | "dev": true, 168 | "requires": { 169 | "regenerator-runtime": "^0.13.4" 170 | } 171 | }, 172 | "@testing-library/react-hooks": { 173 | "version": "8.0.1", 174 | "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz", 175 | "integrity": "sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==", 176 | "dev": true, 177 | "requires": { 178 | "@babel/runtime": "^7.12.5", 179 | "react-error-boundary": "^3.1.0" 180 | } 181 | }, 182 | "js-tokens": { 183 | "version": "4.0.0", 184 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 185 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 186 | "dev": true 187 | }, 188 | "loose-envify": { 189 | "version": "1.4.0", 190 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 191 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 192 | "dev": true, 193 | "requires": { 194 | "js-tokens": "^3.0.0 || ^4.0.0" 195 | } 196 | }, 197 | "object-assign": { 198 | "version": "4.1.1", 199 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 200 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 201 | "dev": true 202 | }, 203 | "react": { 204 | "version": "17.0.2", 205 | "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", 206 | "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", 207 | "dev": true, 208 | "requires": { 209 | "loose-envify": "^1.1.0", 210 | "object-assign": "^4.1.1" 211 | } 212 | }, 213 | "react-error-boundary": { 214 | "version": "3.1.4", 215 | "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", 216 | "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", 217 | "dev": true, 218 | "requires": { 219 | "@babel/runtime": "^7.12.5" 220 | } 221 | }, 222 | "react-is": { 223 | "version": "17.0.2", 224 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", 225 | "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", 226 | "dev": true 227 | }, 228 | "react-shallow-renderer": { 229 | "version": "16.15.0", 230 | "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", 231 | "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", 232 | "dev": true, 233 | "requires": { 234 | "object-assign": "^4.1.1", 235 | "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" 236 | } 237 | }, 238 | "react-test-renderer": { 239 | "version": "17.0.2", 240 | "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz", 241 | "integrity": "sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ==", 242 | "dev": true, 243 | "requires": { 244 | "object-assign": "^4.1.1", 245 | "react-is": "^17.0.2", 246 | "react-shallow-renderer": "^16.13.1", 247 | "scheduler": "^0.20.2" 248 | } 249 | }, 250 | "regenerator-runtime": { 251 | "version": "0.13.9", 252 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", 253 | "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", 254 | "dev": true 255 | }, 256 | "scheduler": { 257 | "version": "0.20.2", 258 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", 259 | "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", 260 | "dev": true, 261 | "requires": { 262 | "loose-envify": "^1.1.0", 263 | "object-assign": "^4.1.1" 264 | } 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /test/react-17/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@testing-library/react-hooks": "^8.0.1", 4 | "react": "^17.0.2", 5 | "react-test-renderer": "^17.0.2" 6 | }, 7 | "scripts": { 8 | "test": "jest" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/react-17/useEvent.react-17.test.ts: -------------------------------------------------------------------------------- 1 | import "../../src/useEvent.test-react-17"; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "ES2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "dist/esm", /* Redirect output structure to the directory. */ 18 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | // "resolveJsonModule": true, 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | 67 | /* Advanced Options */ 68 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 69 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 70 | }, 71 | "exclude": [ 72 | "**/*.test*", 73 | "dist/**" 74 | ] 75 | } 76 | --------------------------------------------------------------------------------