├── .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 |
--------------------------------------------------------------------------------