react-use-event-hook
Advanced tools
Comparing version
@@ -6,6 +6,6 @@ "use strict"; | ||
/** | ||
* Suppress the warning when using useLayoutEffect with SSR | ||
* https://reactjs.org/link/uselayouteffect-ssr | ||
* Suppress the warning when using useLayoutEffect with SSR. (https://reactjs.org/link/uselayouteffect-ssr) | ||
* Make use of useInsertionEffect if available. | ||
*/ | ||
var useBrowserLayoutEffect = typeof window !== "undefined" ? react_1.useLayoutEffect : function () { }; | ||
var useBrowserEffect = typeof window !== "undefined" ? react_1.useInsertionEffect !== null && react_1.useInsertionEffect !== void 0 ? react_1.useInsertionEffect : react_1.useLayoutEffect : function () { }; | ||
/** | ||
@@ -19,8 +19,9 @@ * Similar to useCallback, with a few subtle differences: | ||
// Keep track of the latest callback: | ||
var latestRef = react_1.useRef(useEvent_shouldNotBeInvokedBeforeMount); | ||
useBrowserLayoutEffect(function () { | ||
var latestRef = (0, react_1.useRef)(useEvent_shouldNotBeInvokedBeforeMount); | ||
useBrowserEffect(function () { | ||
latestRef.current = callback; | ||
}, [callback]); | ||
// Create a stable callback that always calls the latest callback: | ||
var stableRef = react_1.useRef(null); | ||
// using useRef instead of useCallback avoids creating and empty array on every render | ||
var stableRef = (0, react_1.useRef)(null); | ||
if (!stableRef.current) { | ||
@@ -27,0 +28,0 @@ stableRef.current = function () { |
{ | ||
"name": "react-use-event-hook", | ||
"version": "0.9.2", | ||
"version": "0.9.3", | ||
"description": "Same as React's `useCallback`, but returns a stable reference.", | ||
@@ -43,8 +43,7 @@ "main": "dist/useEvent.js", | ||
"devDependencies": { | ||
"@testing-library/react-hooks": "^3.4.2", | ||
"@testing-library/react": "^13.4.0", | ||
"@types/jest": "^26.0.20", | ||
"jest": "^26.6.3", | ||
"prettier": "^2.2.1", | ||
"react": "^17.0.1", | ||
"react-test-renderer": "^17.0.1", | ||
"react": "^18.2.0", | ||
"rimraf": "^3.0.2", | ||
@@ -51,0 +50,0 @@ "ts-jest": "^26.4.4", |
@@ -1,5 +0,9 @@ | ||
import { renderHook } from '@testing-library/react-hooks'; | ||
import { useEvent } from './useEvent'; | ||
import React from "react"; | ||
import { renderHook } from "@testing-library/react"; | ||
import { useEvent } from "./useEvent"; | ||
describe('useEvent', () => { | ||
// Only available in React 18+ | ||
const reactSupportsUseInsertionEffect = !!React.useInsertionEffect; | ||
describe(`useEvent (React ${React.version})`, () => { | ||
let initialCallback = jest.fn((...args) => args); | ||
@@ -10,9 +14,8 @@ let stableCallback: jest.Mock; | ||
function renderTestHook() { | ||
const result = | ||
(renderHook( | ||
(latestCallback) => { | ||
stableCallback = useEvent(latestCallback); | ||
}, | ||
{ initialProps: initialCallback } | ||
)); | ||
const result = renderHook( | ||
(latestCallback) => { | ||
stableCallback = useEvent(latestCallback); | ||
}, | ||
{ initialProps: initialCallback } | ||
); | ||
rerender = result.rerender; | ||
@@ -26,4 +29,4 @@ } | ||
it('should return a different function', () => { | ||
expect(typeof stableCallback).toEqual('function'); | ||
it("should return a different function", () => { | ||
expect(typeof stableCallback).toEqual("function"); | ||
expect(stableCallback).not.toBe(initialCallback); | ||
@@ -33,3 +36,3 @@ expect(initialCallback).not.toHaveBeenCalled(); | ||
it('calling the stableCallback should call the initialCallback', () => { | ||
it("calling the stableCallback should call the initialCallback", () => { | ||
stableCallback(); | ||
@@ -39,9 +42,9 @@ expect(initialCallback).toHaveBeenCalled(); | ||
it('all params and return value should be passed through', () => { | ||
it("all params and return value should be passed through", () => { | ||
const returnValue = stableCallback(1, 2, 3); | ||
expect(initialCallback).toHaveBeenCalledWith(1, 2, 3); | ||
expect(returnValue).toEqual([ 1, 2, 3 ]); | ||
expect(returnValue).toEqual([1, 2, 3]); | ||
}); | ||
it('will pass through the current "this" value', async () => { | ||
it('will pass through the current "this" value', () => { | ||
const thisObj = { stableCallback }; | ||
@@ -53,3 +56,108 @@ thisObj.stableCallback(1, 2, 3); | ||
describe('when the hook is rerendered', () => { | ||
describe("timing", () => { | ||
beforeEach(() => { | ||
jest.spyOn(console, "error").mockImplementation(() => { | ||
/* suppress Reacts error logging */ | ||
}); | ||
}); | ||
afterEach(() => { | ||
jest.restoreAllMocks(); | ||
}); | ||
it("will throw an error if called during render", () => { | ||
const useEventBeforeMount = () => { | ||
const cb = useEvent(() => 5); | ||
cb(); | ||
}; | ||
expect(() => { | ||
const r = renderHook(() => useEventBeforeMount()); | ||
// @ts-expect-error This is just for React 17: | ||
if (r.result.error) throw r.result.error; | ||
}).toThrowErrorMatchingInlineSnapshot( | ||
`"INVALID_USEEVENT_INVOCATION: the callback from useEvent cannot be invoked before the component has mounted."` | ||
); | ||
}); | ||
it("will work fine if called inside a useLayoutEffect", () => { | ||
const useEventInLayoutEffect = () => { | ||
const [state, setState] = React.useState(0); | ||
const cb = useEvent(() => 5); | ||
React.useLayoutEffect(() => { | ||
setState(cb()); | ||
}, []); | ||
return state; | ||
}; | ||
const { result } = renderHook(() => useEventInLayoutEffect()); | ||
expect(result).toMatchObject({ current: 5 }); | ||
}); | ||
describe("when used in a NESTED useLayoutEffect", () => { | ||
const renderNestedTest = () => { | ||
/** | ||
* This is a tricky edge-case scenario that happens in React 16/17. | ||
* | ||
* We update our callback inside a `useLayoutEffect`. | ||
* With nested React components, `useLayoutEffect` gets called | ||
* in children first, parents last. | ||
* | ||
* So if we pass a `useEvent` callback into a child component, | ||
* and the child component calls it in a useLayoutEffect, | ||
* we will throw an error. | ||
*/ | ||
// Since we're testing this with react-hooks, we need to use a Context to achieve parent-child hierarchy | ||
const ctx = React.createContext<{ callback(): number }>(null!); | ||
const wrapper: React.FC<React.PropsWithChildren> = (props) => { | ||
const callback = useEvent(() => 5); | ||
return React.createElement(ctx.Provider, { value: { callback } }, props.children); | ||
}; | ||
const { result } = renderHook( | ||
() => { | ||
const [layoutResult, setLayoutResult] = React.useState<any>(null); | ||
const { callback } = React.useContext(ctx); | ||
React.useLayoutEffect(() => { | ||
// Unfortunately, renderHook won't capture a layout error. | ||
// Instead, we'll manually capture it: | ||
try { | ||
setLayoutResult({ callbackResult: callback() }); | ||
} catch (err) { | ||
setLayoutResult({ layoutError: err }); | ||
} | ||
}, []); | ||
return layoutResult; | ||
}, | ||
{ wrapper } | ||
); | ||
return result; | ||
}; | ||
if (!reactSupportsUseInsertionEffect) { | ||
// React 17 | ||
it("will throw an error", () => { | ||
const result = renderNestedTest(); | ||
expect(result.current).toMatchInlineSnapshot(` | ||
Object { | ||
"layoutError": [Error: INVALID_USEEVENT_INVOCATION: the callback from useEvent cannot be invoked before the component has mounted.], | ||
} | ||
`); | ||
}); | ||
} else { | ||
// React 18+ | ||
it("will have no problems because of useInjectionEffect", () => { | ||
const result = renderNestedTest(); | ||
expect(result.current).toMatchInlineSnapshot(` | ||
Object { | ||
"callbackResult": 5, | ||
} | ||
`); | ||
}); | ||
} | ||
}); | ||
}); | ||
describe("when the hook is rerendered", () => { | ||
let newCallback = jest.fn(); | ||
@@ -62,7 +170,7 @@ let originalStableCallback: typeof stableCallback; | ||
it('the stableCallback is stable', () => { | ||
it("the stableCallback is stable", () => { | ||
expect(stableCallback).toBe(originalStableCallback); | ||
}); | ||
it('calling the stableCallback only calls the latest callback', () => { | ||
it("calling the stableCallback only calls the latest callback", () => { | ||
stableCallback(); | ||
@@ -73,3 +181,3 @@ expect(initialCallback).not.toHaveBeenCalled(); | ||
it('the same goes for the 3rd render, etc', () => { | ||
it("the same goes for the 3rd render, etc", () => { | ||
const thirdCallback = jest.fn(); | ||
@@ -76,0 +184,0 @@ rerender(thirdCallback); |
@@ -1,2 +0,6 @@ | ||
import { useLayoutEffect, useRef } from "react"; | ||
import { | ||
useLayoutEffect, | ||
useRef, | ||
useInsertionEffect, // Only available in React 18+ | ||
} from "react"; | ||
@@ -6,6 +10,6 @@ type AnyFunction = (...args: any[]) => any; | ||
/** | ||
* Suppress the warning when using useLayoutEffect with SSR | ||
* https://reactjs.org/link/uselayouteffect-ssr | ||
* Suppress the warning when using useLayoutEffect with SSR. (https://reactjs.org/link/uselayouteffect-ssr) | ||
* Make use of useInsertionEffect if available. | ||
*/ | ||
const useBrowserLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : () => {}; | ||
const useBrowserEffect = typeof window !== "undefined" ? useInsertionEffect ?? useLayoutEffect : () => {}; | ||
@@ -21,3 +25,3 @@ /** | ||
const latestRef = useRef<TCallback>(useEvent_shouldNotBeInvokedBeforeMount as any); | ||
useBrowserLayoutEffect(() => { | ||
useBrowserEffect(() => { | ||
latestRef.current = callback; | ||
@@ -27,5 +31,6 @@ }, [callback]); | ||
// Create a stable callback that always calls the latest callback: | ||
// using useRef instead of useCallback avoids creating and empty array on every render | ||
const stableRef = useRef<TCallback>(null as any); | ||
if (!stableRef.current) { | ||
stableRef.current = function(this: any) { | ||
stableRef.current = function (this: any) { | ||
return latestRef.current.apply(this, arguments as any); | ||
@@ -32,0 +37,0 @@ } as TCallback; |
Sorry, the diff of this file is not supported yet
14435
41.22%8
-11.11%254
68.21%