From ea0673501a57b1ee99c80f2c628760ef53e16cfc Mon Sep 17 00:00:00 2001
From: Sergey Kozlov <dartess@mail.ru>
Date: Thu, 25 Aug 2022 13:13:23 +0300
Subject: [PATCH 01/15] add utils for run tests of hooks; fix usage render from
 @reach-internal/test instead of @testing-library/react

---
 internal/test/types.ts  | 24 +++++++++++++++++++++++-
 internal/test/utils.tsx | 22 ++++++++++++++++++++--
 package.json            |  1 +
 pnpm-lock.yaml          | 35 +++++++++++++++++++++++++++++++++++
 test/alias.ts           |  1 +
 5 files changed, 80 insertions(+), 3 deletions(-)

diff --git a/internal/test/types.ts b/internal/test/types.ts
index a986fd3ec..3608a9296 100644
--- a/internal/test/types.ts
+++ b/internal/test/types.ts
@@ -3,8 +3,16 @@ import type {
 	RenderOptions as TLRenderOptions,
 	RenderResult as TLRenderResult,
 } from "@testing-library/react";
+import type {
+	RenderHookOptions as TLLegacyRenderHookOptions,
+	RenderHookResult as TLLegacyRenderHookResult,
+} from "@testing-library/react-hooks";
+import type {
+	RenderHookOptions as TLActualRenderHookOptions,
+	RenderHookResult as TLActualRenderHookResult,
+} from "@testing-library/react-13";
 
-export type RenderOptions = Omit<TLRenderOptions, "queries"> & {
+export type RenderOptions = Omit<TLRenderOptions, "queries" | "wrapper"> & {
 	strict?: boolean;
 };
 
@@ -15,3 +23,17 @@ export type RenderResult<
 	setProps(props: P): RenderResult<P, T>;
 	forceUpdate(): RenderResult<P, T>;
 };
+
+export type RenderHookOptions<TProps> = Omit<
+	TLLegacyRenderHookOptions<TProps> &
+		TLActualRenderHookOptions<TProps> & {
+			strict?: boolean;
+		},
+	"wrapper"
+>;
+
+export type RenderHookResult<TResult, TProps> = TLLegacyRenderHookResult<
+	TProps,
+	TResult
+> &
+	TLActualRenderHookResult<TResult, TProps>;
diff --git a/internal/test/utils.tsx b/internal/test/utils.tsx
index 9b5d34252..1c37c7481 100644
--- a/internal/test/utils.tsx
+++ b/internal/test/utils.tsx
@@ -2,9 +2,15 @@ import * as React from "react";
 import { act } from "react-dom/test-utils";
 import type { MatcherFunction } from "@testing-library/react";
 import { render as tlRender, fireEvent } from "@testing-library/react";
+import { renderHook as tlRenderHook } from "@testing-library/react-hooks";
 import { fireEvent as fireDomEvent } from "@testing-library/dom";
 import userEvent from "@testing-library/user-event";
-import type { RenderOptions, RenderResult } from "./types";
+import type {
+	RenderHookOptions,
+	RenderHookResult,
+	RenderOptions,
+	RenderResult,
+} from "./types";
 
 /**
  * This function is useful if you want to query a DOM element by its text
@@ -79,6 +85,17 @@ export function render<
 	return result;
 }
 
+export function renderHook<TProps, TResult>(
+	callback: (props: TProps) => TResult,
+	options: RenderHookOptions<TProps> = {}
+): RenderHookResult<TResult, TProps> {
+	const { strict = false, ...restOptions } = options;
+	return tlRenderHook(callback, {
+		...restOptions,
+		wrapper: strict ? React.StrictMode : React.Fragment,
+	});
+}
+
 export async function wait(time: number) {
 	return await new Promise<void>((res) => setTimeout(res, time));
 }
@@ -125,6 +142,7 @@ export function simulateEnterKeyClick(
 
 type Query = (f: MatcherFunction) => HTMLElement | null;
 
-export * from "@testing-library/react";
+export { cleanup as cleanupHooks } from "@testing-library/react-hooks";
+export { cleanup, fireEvent, screen } from "@testing-library/react";
 export { act, userEvent, fireDomEvent };
 export type { RenderOptions, RenderResult };
diff --git a/package.json b/package.json
index 6f4ae9d1d..ab02f27f9 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
 		"@testing-library/dom": "^8.16.0",
 		"@testing-library/react": "^12.1.5",
 		"@testing-library/react-13": "npm:@testing-library/react@^13.3.0",
+		"@testing-library/react-hooks": "^8.0.1",
 		"@testing-library/user-event": "^14.2.1",
 		"@types/aria-query": "^5.0.0",
 		"@types/css": "^0.0.33",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6b7bbe496..73653ff21 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -32,6 +32,7 @@ importers:
       '@testing-library/dom': ^8.16.0
       '@testing-library/react': ^12.1.5
       '@testing-library/react-13': npm:@testing-library/react@^13.3.0
+      '@testing-library/react-hooks': ^8.0.1
       '@testing-library/user-event': ^14.2.1
       '@types/aria-query': ^5.0.0
       '@types/css': ^0.0.33
@@ -109,6 +110,7 @@ importers:
       '@testing-library/dom': 8.16.0
       '@testing-library/react': 12.1.5_sfoxds7t5ydpegc3knd667wn6m
       '@testing-library/react-13': /@testing-library/react/13.3.0_sfoxds7t5ydpegc3knd667wn6m
+      '@testing-library/react-hooks': 8.0.1_nn45z5sr7igu7sfun6tiae5hx4
       '@testing-library/user-event': 14.2.1_gwcpuyfvwbszhlmedmugzivgzu
       '@types/aria-query': 5.0.0
       '@types/css': 0.0.33
@@ -3658,6 +3660,29 @@ packages:
       pretty-format: 27.5.1
     dev: false
 
+  /@testing-library/react-hooks/8.0.1_nn45z5sr7igu7sfun6tiae5hx4:
+    resolution: {integrity: sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==}
+    engines: {node: '>=12'}
+    peerDependencies:
+      '@types/react': ^16.9.0 || ^17.0.0
+      react: ^16.9.0 || ^17.0.0
+      react-dom: ^16.9.0 || ^17.0.0
+      react-test-renderer: ^16.9.0 || ^17.0.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      react-dom:
+        optional: true
+      react-test-renderer:
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.18.6
+      '@types/react': 17.0.47
+      react: 17.0.2
+      react-dom: 17.0.2_react@17.0.2
+      react-error-boundary: 3.1.4_react@17.0.2
+    dev: false
+
   /@testing-library/react/12.1.5_sfoxds7t5ydpegc3knd667wn6m:
     resolution: {integrity: sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg==}
     engines: {node: '>=12'}
@@ -10723,6 +10748,16 @@ packages:
       react-is: 17.0.2
     dev: false
 
+  /react-error-boundary/3.1.4_react@17.0.2:
+    resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==}
+    engines: {node: '>=10', npm: '>=6'}
+    peerDependencies:
+      react: '>=16.13.1'
+    dependencies:
+      '@babel/runtime': 7.18.6
+      react: 17.0.2
+    dev: false
+
   /react-focus-lock/2.5.2_react@17.0.2:
     resolution: {integrity: sha512-WzpdOnEqjf+/A3EH9opMZWauag7gV0BxFl+EY4ElA4qFqYsUsBLnmo2sELbN5OC30S16GAWMy16B9DLPpdJKAQ==}
     peerDependencies:
diff --git a/test/alias.ts b/test/alias.ts
index 84660132f..e84c2742f 100644
--- a/test/alias.ts
+++ b/test/alias.ts
@@ -14,6 +14,7 @@ if (reactVersion === 16) {
 		"react-dom": "react-dom-18",
 		"react-is": "react-is-18",
 		"@testing-library/react": "@testing-library/react-13",
+		"@testing-library/react-hooks": "@testing-library/react-13",
 	};
 }
 

From fd852dbe8f61cb6d1eed2a10f62335780a6e402a Mon Sep 17 00:00:00 2001
From: Sergey Kozlov <dartess@mail.ru>
Date: Thu, 25 Aug 2022 13:30:45 +0300
Subject: [PATCH 02/15] add tests for useConstant

---
 packages/utils/__tests__/use-constant.test.ts | 26 +++++++++++++++++++
 1 file changed, 26 insertions(+)
 create mode 100644 packages/utils/__tests__/use-constant.test.ts

diff --git a/packages/utils/__tests__/use-constant.test.ts b/packages/utils/__tests__/use-constant.test.ts
new file mode 100644
index 000000000..727ea96e2
--- /dev/null
+++ b/packages/utils/__tests__/use-constant.test.ts
@@ -0,0 +1,26 @@
+import { afterEach, describe, expect, it } from "vitest";
+import { renderHook, cleanupHooks } from "@reach-internal/test/utils";
+import { useConstant } from "@reach/utils";
+
+afterEach(cleanupHooks);
+
+describe("useConstant", () => {
+	const renderUseConstant = () =>
+		renderHook(() => useConstant(() => ({ foo: "bar" })));
+
+	it("should return value from callback", () => {
+		const render = renderUseConstant();
+
+		const firstRenderedObject = render.result.current;
+		expect(firstRenderedObject).toEqual({ foo: "bar" });
+	});
+
+	it("should return the same value after rerender", () => {
+		const render = renderUseConstant();
+		const resultFirst = render.result.current;
+		render.rerender();
+		const resultSecond = render.result.current;
+
+		expect(resultFirst).toBe(resultSecond);
+	});
+});

From d58d027fac7cbb7e3f75d0be20d65ae1549863c9 Mon Sep 17 00:00:00 2001
From: Sergey Kozlov <dartess@mail.ru>
Date: Thu, 25 Aug 2022 16:12:08 +0300
Subject: [PATCH 03/15] add tests for useControlledState

---
 internal/test/utils.tsx                       | 12 ++---
 .../__tests__/use-controlled-state.test.ts    | 50 +++++++++++++++++++
 2 files changed, 56 insertions(+), 6 deletions(-)
 create mode 100644 packages/utils/__tests__/use-controlled-state.test.ts

diff --git a/internal/test/utils.tsx b/internal/test/utils.tsx
index 1c37c7481..836e9c9ef 100644
--- a/internal/test/utils.tsx
+++ b/internal/test/utils.tsx
@@ -1,10 +1,7 @@
 import * as React from "react";
-import { act } from "react-dom/test-utils";
 import type { MatcherFunction } from "@testing-library/react";
 import { render as tlRender, fireEvent } from "@testing-library/react";
 import { renderHook as tlRenderHook } from "@testing-library/react-hooks";
-import { fireEvent as fireDomEvent } from "@testing-library/dom";
-import userEvent from "@testing-library/user-event";
 import type {
 	RenderHookOptions,
 	RenderHookResult,
@@ -142,7 +139,10 @@ export function simulateEnterKeyClick(
 
 type Query = (f: MatcherFunction) => HTMLElement | null;
 
-export { cleanup as cleanupHooks } from "@testing-library/react-hooks";
-export { cleanup, fireEvent, screen } from "@testing-library/react";
-export { act, userEvent, fireDomEvent };
+export {
+	cleanup as cleanupHooks,
+	act as actHooks,
+} from "@testing-library/react-hooks";
+export { cleanup, fireEvent, screen, act } from "@testing-library/react";
+export * as userEvent from "@testing-library/user-event";
 export type { RenderOptions, RenderResult };
diff --git a/packages/utils/__tests__/use-controlled-state.test.ts b/packages/utils/__tests__/use-controlled-state.test.ts
new file mode 100644
index 000000000..9cc2c11d8
--- /dev/null
+++ b/packages/utils/__tests__/use-controlled-state.test.ts
@@ -0,0 +1,50 @@
+import { afterEach, describe, expect, it } from "vitest";
+import { renderHook, cleanupHooks, actHooks } from "@reach-internal/test/utils";
+import { useControlledState } from "@reach/utils";
+
+afterEach(cleanupHooks);
+
+describe("useControlledState", () => {
+	const DEFAULT_VALUE = 10;
+	const CONTROLLED_VALUE = 42;
+
+	it("should return value and setter", () => {
+		const { result } = renderHook(() =>
+			useControlledState({
+				defaultValue: DEFAULT_VALUE,
+				controlledValue: undefined,
+			})
+		);
+
+		expect(result.current[0]).toBe(DEFAULT_VALUE);
+		expect(typeof result.current[1]).toBe("function");
+	});
+
+	it("should work as uncontrolled", () => {
+		const { result } = renderHook(() =>
+			useControlledState({
+				defaultValue: DEFAULT_VALUE,
+				controlledValue: undefined,
+			})
+		);
+		expect(result.current[0]).toBe(DEFAULT_VALUE);
+		actHooks(() => {
+			result.current[1](17);
+		});
+		expect(result.current[0]).toBe(17);
+	});
+
+	it("should work as controlled", () => {
+		const { result } = renderHook(() =>
+			useControlledState({
+				defaultValue: DEFAULT_VALUE,
+				controlledValue: CONTROLLED_VALUE,
+			})
+		);
+		expect(result.current[0]).toBe(CONTROLLED_VALUE);
+		actHooks(() => {
+			result.current[1](17);
+		});
+		expect(result.current[0]).toBe(CONTROLLED_VALUE);
+	});
+});

From e62e04f247bb119855c7ef46f6e6f31a49efbf39 Mon Sep 17 00:00:00 2001
From: Sergey Kozlov <dartess@mail.ru>
Date: Thu, 25 Aug 2022 17:10:29 +0300
Subject: [PATCH 04/15] add tests for useEventListener

---
 .../__tests__/use-event-listener.test.tsx     | 33 +++++++++++++++++++
 1 file changed, 33 insertions(+)
 create mode 100644 packages/utils/__tests__/use-event-listener.test.tsx

diff --git a/packages/utils/__tests__/use-event-listener.test.tsx b/packages/utils/__tests__/use-event-listener.test.tsx
new file mode 100644
index 000000000..c81fcf398
--- /dev/null
+++ b/packages/utils/__tests__/use-event-listener.test.tsx
@@ -0,0 +1,33 @@
+import * as React from "react";
+import { render, fireEvent, cleanup } from "@reach-internal/test/utils";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { useEventListener } from "@reach/utils";
+
+afterEach(cleanup);
+
+describe("useEventListener", () => {
+	const Test = ({ onBodyClick }: { onBodyClick: () => void }) => {
+		useEventListener("click", onBodyClick, document.body);
+		return null;
+	};
+
+	it("should call event listener when it's need", () => {
+		const handleBodyClick = vi.fn();
+		render(<Test onBodyClick={handleBodyClick} />);
+		fireEvent.click(document.body);
+		expect(handleBodyClick).toHaveBeenCalledTimes(1);
+		fireEvent.click(document.body);
+		expect(handleBodyClick).toHaveBeenCalledTimes(2);
+	});
+
+	it("should can change event listener from args", () => {
+		const handleBodyClick1 = vi.fn();
+		const handleBodyClick2 = vi.fn();
+		const { rerender } = render(<Test onBodyClick={handleBodyClick1} />);
+		fireEvent.click(document.body);
+		rerender(<Test onBodyClick={handleBodyClick2} />);
+		fireEvent.click(document.body);
+		expect(handleBodyClick1).toHaveBeenCalledOnce();
+		expect(handleBodyClick2).toHaveBeenCalledOnce();
+	});
+});

From 030ce698586728882f4cb115c5cd0276eabfa716 Mon Sep 17 00:00:00 2001
From: Sergey Kozlov <dartess@mail.ru>
Date: Thu, 25 Aug 2022 18:40:15 +0300
Subject: [PATCH 05/15] add tests for useFocusChange and fix errors

---
 internal/test/utils.tsx                       |   2 +-
 .../utils/__tests__/use-focus-change.test.tsx | 106 ++++++++++++++++++
 packages/utils/src/use-focus-change.ts        |  20 ++--
 3 files changed, 119 insertions(+), 9 deletions(-)
 create mode 100644 packages/utils/__tests__/use-focus-change.test.tsx

diff --git a/internal/test/utils.tsx b/internal/test/utils.tsx
index 836e9c9ef..4e9a975de 100644
--- a/internal/test/utils.tsx
+++ b/internal/test/utils.tsx
@@ -144,5 +144,5 @@ export {
 	act as actHooks,
 } from "@testing-library/react-hooks";
 export { cleanup, fireEvent, screen, act } from "@testing-library/react";
-export * as userEvent from "@testing-library/user-event";
+export { default as userEvent } from "@testing-library/user-event";
 export type { RenderOptions, RenderResult };
diff --git a/packages/utils/__tests__/use-focus-change.test.tsx b/packages/utils/__tests__/use-focus-change.test.tsx
new file mode 100644
index 000000000..5b75ab2b3
--- /dev/null
+++ b/packages/utils/__tests__/use-focus-change.test.tsx
@@ -0,0 +1,106 @@
+import * as React from "react";
+import { render, cleanup, userEvent } from "@reach-internal/test/utils";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { useFocusChange } from "@reach/utils";
+
+afterEach(cleanup);
+
+describe("useFocusChange", () => {
+	const Test = ({
+		onChange,
+		when,
+	}: {
+		onChange: () => void;
+		when?: "focus" | "blur";
+	}) => {
+		useFocusChange(onChange, when);
+		return (
+			<>
+				<input type="text" placeholder="first" />
+				<input type="text" placeholder="second" />
+				<div>just div</div>
+			</>
+		);
+	};
+
+	const renderTest = (when?: "focus" | "blur") => {
+		const handleChange = vi.fn();
+		const { getByPlaceholderText, getByText } = render(
+			<Test onChange={handleChange} when={when} />
+		);
+		const firstInput = getByPlaceholderText("first");
+		const secondInput = getByPlaceholderText("second");
+		const div = getByText("just div");
+		return {
+			firstInput,
+			secondInput,
+			div,
+			handleChange,
+		};
+	};
+
+	/**
+	 * WARNING: The order of the tests is important:
+	 * the blur test should come first.
+	 * If this is not the case, the activeElement will be dirty
+	 * and the blur event will fire when the input is clicked.
+	 */
+
+	it("should call handler on blur", async () => {
+		const {
+			firstInput,
+			secondInput,
+			div,
+			handleChange: handleBlur,
+		} = renderTest("blur");
+
+		await userEvent.click(firstInput);
+		expect(handleBlur).not.toHaveBeenCalled();
+
+		await userEvent.click(secondInput);
+		expect(handleBlur).toHaveBeenCalledTimes(1);
+		expect(handleBlur).toHaveBeenCalledWith(
+			document.body,
+			document.body,
+			expect.any(FocusEvent)
+		);
+
+		await userEvent.click(div);
+		expect(handleBlur).toHaveBeenCalledTimes(2);
+		expect(handleBlur).toHaveBeenCalledWith(
+			document.body,
+			document.body,
+			expect.any(FocusEvent)
+		);
+	});
+
+	it("should call handler on focus", async () => {
+		const { firstInput, secondInput, handleChange: handleFocus } = renderTest();
+
+		await userEvent.click(firstInput);
+		expect(handleFocus).toHaveBeenCalledTimes(1);
+		expect(handleFocus).toHaveBeenCalledWith(
+			firstInput,
+			document.body,
+			expect.any(FocusEvent)
+		);
+
+		await userEvent.click(secondInput);
+		expect(handleFocus).toHaveBeenCalledTimes(2);
+		expect(handleFocus).toHaveBeenCalledWith(
+			secondInput,
+			firstInput,
+			expect.any(FocusEvent)
+		);
+	});
+
+	it("should do not call handler on focus at the same node", async () => {
+		const { firstInput, handleChange: handleFocus } = renderTest();
+
+		await userEvent.click(firstInput);
+		expect(handleFocus).toHaveBeenCalledOnce();
+
+		await userEvent.click(firstInput);
+		expect(handleFocus).toHaveBeenCalledOnce();
+	});
+});
diff --git a/packages/utils/src/use-focus-change.ts b/packages/utils/src/use-focus-change.ts
index 86da9057a..e0944bb05 100644
--- a/packages/utils/src/use-focus-change.ts
+++ b/packages/utils/src/use-focus-change.ts
@@ -22,20 +22,24 @@ export function useFocusChange(
 		lastActiveElement.current = ownerDocument.activeElement;
 
 		function onChange(event: FocusEvent) {
-			if (lastActiveElement.current !== ownerDocument.activeElement) {
-				handleChange(
-					ownerDocument.activeElement,
-					lastActiveElement.current,
-					event
-				);
-				lastActiveElement.current = ownerDocument.activeElement;
+			if (
+				when === "focus" &&
+				lastActiveElement.current === ownerDocument.activeElement
+			) {
+				return;
 			}
+			handleChange(
+				ownerDocument.activeElement,
+				lastActiveElement.current,
+				event
+			);
+			lastActiveElement.current = ownerDocument.activeElement;
 		}
 
 		ownerDocument.addEventListener(when, onChange, true);
 
 		return () => {
-			ownerDocument.removeEventListener(when, onChange);
+			ownerDocument.removeEventListener(when, onChange, true);
 		};
 	}, [when, handleChange, ownerDocument]);
 }

From 711118009a2cc1036dfbd406bbf7c84505ffc599 Mon Sep 17 00:00:00 2001
From: Sergey Kozlov <dartess@mail.ru>
Date: Thu, 25 Aug 2022 19:35:48 +0300
Subject: [PATCH 06/15] add tests for useForceUpdate

---
 .../utils/__tests__/use-force-update.test.tsx | 34 +++++++++++++++++++
 1 file changed, 34 insertions(+)
 create mode 100644 packages/utils/__tests__/use-force-update.test.tsx

diff --git a/packages/utils/__tests__/use-force-update.test.tsx b/packages/utils/__tests__/use-force-update.test.tsx
new file mode 100644
index 000000000..7aa7c1db7
--- /dev/null
+++ b/packages/utils/__tests__/use-force-update.test.tsx
@@ -0,0 +1,34 @@
+/// <reference types="vitest-dom/extend-expect" />
+
+import * as React from "react";
+import { render, cleanup, userEvent } from "@reach-internal/test/utils";
+import { afterEach, describe, expect, it } from "vitest";
+import { useForceUpdate } from "@reach/utils";
+
+afterEach(cleanup);
+
+describe("useForceUpdate", () => {
+	it("should force rerender when called", async () => {
+		let nonObservableVariable = "foo";
+
+		const Test = () => {
+			const forceUpdate = useForceUpdate();
+			return (
+				<>
+					<div data-testid="div">{nonObservableVariable}</div>
+					<button data-testid="button" onClick={forceUpdate} />
+				</>
+			);
+		};
+
+		const { getByTestId } = render(<Test />);
+		const div = getByTestId("div");
+		const button = getByTestId("button");
+
+		expect(div).toHaveTextContent("foo");
+		nonObservableVariable = "bar";
+		expect(div).toHaveTextContent("foo");
+		await userEvent.click(button);
+		expect(div).toHaveTextContent("bar");
+	});
+});

From 51cac7f49ca6e36f493b291afe7703bedf09cb22 Mon Sep 17 00:00:00 2001
From: Sergey Kozlov <dartess@mail.ru>
Date: Thu, 25 Aug 2022 19:49:28 +0300
Subject: [PATCH 07/15] add tests for useLazyRef

---
 packages/utils/__tests__/use-lazy-ref.test.ts | 26 +++++++++++++++++++
 packages/utils/src/use-lazy-ref.ts            | 10 +++----
 2 files changed, 31 insertions(+), 5 deletions(-)
 create mode 100644 packages/utils/__tests__/use-lazy-ref.test.ts

diff --git a/packages/utils/__tests__/use-lazy-ref.test.ts b/packages/utils/__tests__/use-lazy-ref.test.ts
new file mode 100644
index 000000000..f77a8af68
--- /dev/null
+++ b/packages/utils/__tests__/use-lazy-ref.test.ts
@@ -0,0 +1,26 @@
+import { afterEach, describe, expect, it } from "vitest";
+import { renderHook, cleanupHooks } from "@reach-internal/test/utils";
+import { useLazyRef } from "@reach/utils";
+
+afterEach(cleanupHooks);
+
+describe("useLazyRef", () => {
+	const renderUseLazyRef = () =>
+		renderHook(() => useLazyRef(() => ({ foo: "bar" })));
+
+	it("should return value from callback", () => {
+		const render = renderUseLazyRef();
+
+		const firstRenderedObject = render.result.current.current;
+		expect(firstRenderedObject).toEqual({ foo: "bar" });
+	});
+
+	it("should return the same value after rerender", () => {
+		const render = renderUseLazyRef();
+		const resultFirst = render.result.current.current;
+		render.rerender();
+		const resultSecond = render.result.current.current;
+
+		expect(resultFirst).toBe(resultSecond);
+	});
+});
diff --git a/packages/utils/src/use-lazy-ref.ts b/packages/utils/src/use-lazy-ref.ts
index e272730ca..21051a601 100644
--- a/packages/utils/src/use-lazy-ref.ts
+++ b/packages/utils/src/use-lazy-ref.ts
@@ -1,14 +1,14 @@
 import { useRef } from "react";
 import type * as React from "react";
 
-export function useLazyRef<F extends (...args: any[]) => any>(
-	fn: F
-): React.MutableRefObject<ReturnType<F>> {
+export function useLazyRef<ValueType>(
+	fn: () => ValueType
+): React.MutableRefObject<ValueType> {
 	let isSet = useRef(false);
-	let ref = useRef<any>();
+	let ref = useRef<ValueType>();
 	if (!isSet.current) {
 		isSet.current = true;
 		ref.current = fn();
 	}
-	return ref;
+	return ref as React.MutableRefObject<ValueType>;
 }

From 6ec6bec188f7ecf2b7d680abb8f79b1bd15a3dce Mon Sep 17 00:00:00 2001
From: Sergey Kozlov <dartess@mail.ru>
Date: Thu, 25 Aug 2022 20:20:59 +0300
Subject: [PATCH 08/15] add tests for usePrevious

---
 .../utils/__tests__/use-previous.test.tsx     | 42 +++++++++++++++++++
 1 file changed, 42 insertions(+)
 create mode 100644 packages/utils/__tests__/use-previous.test.tsx

diff --git a/packages/utils/__tests__/use-previous.test.tsx b/packages/utils/__tests__/use-previous.test.tsx
new file mode 100644
index 000000000..b37ee4d6c
--- /dev/null
+++ b/packages/utils/__tests__/use-previous.test.tsx
@@ -0,0 +1,42 @@
+/// <reference types="vitest-dom/extend-expect" />
+
+import * as React from "react";
+import { render, cleanup, userEvent } from "@reach-internal/test/utils";
+import { afterEach, describe, expect, it } from "vitest";
+import { usePrevious } from "@reach/utils";
+
+afterEach(cleanup);
+
+describe.only("usePrevious", () => {
+	it("should return previous value", async () => {
+		const Test = () => {
+			const [state, setState] = React.useState("foo");
+			const previousState = usePrevious(state);
+			return (
+				<>
+					<div data-testid="previousState">
+						{state}
+						{previousState}
+					</div>
+					<div data-testid="currentState">{state}</div>
+					<button data-testid="buttonFoo" onClick={() => setState("foo")} />
+					<button data-testid="buttonBar" onClick={() => setState("bar")} />
+				</>
+			);
+		};
+
+		const { getByTestId } = render(<Test />);
+		const divCurrentState = getByTestId("currentState");
+		const divPreviousState = getByTestId("previousState");
+		const buttonFoo = getByTestId("buttonFoo");
+		const buttonBar = getByTestId("buttonBar");
+
+		await userEvent.click(buttonBar);
+		expect(divCurrentState).toHaveTextContent("bar");
+		expect(divPreviousState).toHaveTextContent("foo");
+
+		await userEvent.click(buttonFoo);
+		expect(divCurrentState).toHaveTextContent("foo");
+		expect(divPreviousState).toHaveTextContent("bar");
+	});
+});

From 40bd40bd74e24fbd26377ded1c26667dfe8dc6c7 Mon Sep 17 00:00:00 2001
From: Sergey Kozlov <dartess@mail.ru>
Date: Thu, 25 Aug 2022 22:20:15 +0300
Subject: [PATCH 09/15] add tests for useStableCallback and
 useStableLayoutCallback

---
 .../__tests__/use-stable-callback.test.tsx    | 36 +++++++++++++++++++
 1 file changed, 36 insertions(+)
 create mode 100644 packages/utils/__tests__/use-stable-callback.test.tsx

diff --git a/packages/utils/__tests__/use-stable-callback.test.tsx b/packages/utils/__tests__/use-stable-callback.test.tsx
new file mode 100644
index 000000000..c56c3a90b
--- /dev/null
+++ b/packages/utils/__tests__/use-stable-callback.test.tsx
@@ -0,0 +1,36 @@
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { renderHook, cleanupHooks } from "@reach-internal/test/utils";
+import { useStableCallback, useStableLayoutCallback } from "@reach/utils";
+import { useEffect } from "react";
+
+afterEach(cleanupHooks);
+
+describe("useStableCallback", () => {
+	it("should not cause the effect to be called again on change", () => {
+		const mock = vi.fn();
+		const { rerender } = renderHook(() => {
+			const callback = () => mock();
+			const stableCallback = useStableCallback(callback);
+			useEffect(stableCallback, [stableCallback]);
+		});
+
+		expect(mock).toHaveBeenCalledOnce();
+		rerender();
+		expect(mock).toHaveBeenCalledOnce();
+	});
+});
+
+describe("useStableLayoutCallback", () => {
+	it("should not cause the effect to be called again on change", () => {
+		const mock = vi.fn();
+		const { rerender } = renderHook(() => {
+			const callback = () => mock();
+			const stableCallback = useStableLayoutCallback(callback);
+			useEffect(stableCallback, [stableCallback]);
+		});
+
+		expect(mock).toHaveBeenCalledOnce();
+		rerender();
+		expect(mock).toHaveBeenCalledOnce();
+	});
+});

From 79ef7dca9f81109fefa40bcf482fbfa86cd7704d Mon Sep 17 00:00:00 2001
From: Sergey Kozlov <dartess@mail.ru>
Date: Thu, 25 Aug 2022 22:33:11 +0300
Subject: [PATCH 10/15] add tests for useStatefulRefValue

---
 .../__tests__/use-stateful-ref-value.test.ts  | 36 +++++++++++++++++++
 1 file changed, 36 insertions(+)
 create mode 100644 packages/utils/__tests__/use-stateful-ref-value.test.ts

diff --git a/packages/utils/__tests__/use-stateful-ref-value.test.ts b/packages/utils/__tests__/use-stateful-ref-value.test.ts
new file mode 100644
index 000000000..5e3682aa8
--- /dev/null
+++ b/packages/utils/__tests__/use-stateful-ref-value.test.ts
@@ -0,0 +1,36 @@
+import { useRef } from "react";
+import { afterEach, describe, expect, it } from "vitest";
+import { renderHook, cleanupHooks, actHooks } from "@reach-internal/test/utils";
+import { useStatefulRefValue } from "@reach/utils";
+
+afterEach(cleanupHooks);
+
+describe("useStatefulRefValue", () => {
+	it("should return value and setter", () => {
+		const { result } = renderHook(() => {
+			const ref = useRef();
+			return useStatefulRefValue(ref, 10);
+		});
+
+		expect(result.current[0]).toBe(10);
+		expect(typeof result.current[1]).toBe("function");
+	});
+
+	it("should update ref's value", () => {
+		const { result } = renderHook(() => {
+			const ref = useRef();
+			return {
+				statefulRefValue: useStatefulRefValue(ref, 10 as number),
+				ref,
+			};
+		});
+
+		actHooks(() => result.current.statefulRefValue[1](42));
+		expect(result.current.ref.current).toBe(42);
+		expect(result.current.ref.current).toBe(result.current.statefulRefValue[0]);
+
+		actHooks(() => result.current.statefulRefValue[1](100500));
+		expect(result.current.ref.current).toBe(100500);
+		expect(result.current.ref.current).toBe(result.current.statefulRefValue[0]);
+	});
+});

From 8931cd7935dbe94ee3c75a706949c8b6573f0467 Mon Sep 17 00:00:00 2001
From: Sergey Kozlov <dartess@mail.ru>
Date: Thu, 25 Aug 2022 22:40:11 +0300
Subject: [PATCH 11/15] add tests for useUpdateEffect

---
 .../utils/__tests__/use-update-effect.test.ts | 29 +++++++++++++++++++
 1 file changed, 29 insertions(+)
 create mode 100644 packages/utils/__tests__/use-update-effect.test.ts

diff --git a/packages/utils/__tests__/use-update-effect.test.ts b/packages/utils/__tests__/use-update-effect.test.ts
new file mode 100644
index 000000000..a8f278790
--- /dev/null
+++ b/packages/utils/__tests__/use-update-effect.test.ts
@@ -0,0 +1,29 @@
+import { useState } from "react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { renderHook, cleanupHooks, actHooks } from "@reach-internal/test/utils";
+import { useUpdateEffect } from "@reach/utils";
+
+afterEach(cleanupHooks);
+
+describe("useUpdateEffect", () => {
+	it("should do not call effect on mount", () => {
+		const effect = vi.fn();
+		renderHook(() => useUpdateEffect(effect, []));
+
+		expect(effect).not.toHaveBeenCalled();
+	});
+
+	it("should call effect on every update", () => {
+		const effect = vi.fn();
+		const { result } = renderHook(() => {
+			const [dependency, setDependency] = useState(0);
+			useUpdateEffect(effect, [dependency]);
+			return { setDependency };
+		});
+
+		actHooks(() => result.current.setDependency(10));
+		expect(effect).toHaveBeenCalledTimes(1);
+		actHooks(() => result.current.setDependency(22));
+		expect(effect).toHaveBeenCalledTimes(2);
+	});
+});

From 3ccb63c81cd750f305b99f94888b367fbc007a11 Mon Sep 17 00:00:00 2001
From: Sergey Kozlov <dartess@mail.ru>
Date: Thu, 25 Aug 2022 22:42:01 +0300
Subject: [PATCH 12/15] remove unused `only`

---
 packages/utils/__tests__/use-previous.test.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/utils/__tests__/use-previous.test.tsx b/packages/utils/__tests__/use-previous.test.tsx
index b37ee4d6c..681bd74a5 100644
--- a/packages/utils/__tests__/use-previous.test.tsx
+++ b/packages/utils/__tests__/use-previous.test.tsx
@@ -7,7 +7,7 @@ import { usePrevious } from "@reach/utils";
 
 afterEach(cleanup);
 
-describe.only("usePrevious", () => {
+describe("usePrevious", () => {
 	it("should return previous value", async () => {
 		const Test = () => {
 			const [state, setState] = React.useState("foo");

From 1ebec3e22b079ef29dd12c1e07398ca7fbc3ea14 Mon Sep 17 00:00:00 2001
From: Sergey Kozlov <dartess@mail.ru>
Date: Thu, 25 Aug 2022 23:02:41 +0300
Subject: [PATCH 13/15] adopt useUpdateEffect for react@18 StrictMode

---
 packages/utils/src/use-update-effect.ts | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/packages/utils/src/use-update-effect.ts b/packages/utils/src/use-update-effect.ts
index f02c429cf..8c9245ad0 100644
--- a/packages/utils/src/use-update-effect.ts
+++ b/packages/utils/src/use-update-effect.ts
@@ -11,13 +11,18 @@ export function useUpdateEffect(
 	effect: React.EffectCallback,
 	deps?: React.DependencyList
 ) {
-	const mounted = useRef(false);
 	useEffect(() => {
 		if (mounted.current) {
-			effect();
-		} else {
-			mounted.current = true;
+			return effect();
 		}
 		// eslint-disable-next-line react-hooks/exhaustive-deps
 	}, deps);
+
+	const mounted = useRef(false);
+	useEffect(() => {
+		mounted.current = true;
+		return () => {
+			mounted.current = false;
+		};
+	}, []);
 }

From 81db498f1a4232817c57d4f134c4aa95db25bd74 Mon Sep 17 00:00:00 2001
From: Sergey Kozlov <dartess@mail.ru>
Date: Thu, 25 Aug 2022 23:14:53 +0300
Subject: [PATCH 14/15] adopt useStableCallback tests for react@18 StrictMode

---
 packages/utils/__tests__/use-stable-callback.test.tsx | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/packages/utils/__tests__/use-stable-callback.test.tsx b/packages/utils/__tests__/use-stable-callback.test.tsx
index c56c3a90b..3fa7704c2 100644
--- a/packages/utils/__tests__/use-stable-callback.test.tsx
+++ b/packages/utils/__tests__/use-stable-callback.test.tsx
@@ -14,9 +14,10 @@ describe("useStableCallback", () => {
 			useEffect(stableCallback, [stableCallback]);
 		});
 
-		expect(mock).toHaveBeenCalledOnce();
+		const calledTimesBeforeRerender = mock.mock.calls.length;
 		rerender();
-		expect(mock).toHaveBeenCalledOnce();
+		const calledTimesAfterRerender = mock.mock.calls.length;
+		expect(calledTimesBeforeRerender).toBe(calledTimesAfterRerender);
 	});
 });
 
@@ -29,8 +30,9 @@ describe("useStableLayoutCallback", () => {
 			useEffect(stableCallback, [stableCallback]);
 		});
 
-		expect(mock).toHaveBeenCalledOnce();
+		const calledTimesBeforeRerender = mock.mock.calls.length;
 		rerender();
-		expect(mock).toHaveBeenCalledOnce();
+		const calledTimesAfterRerender = mock.mock.calls.length;
+		expect(calledTimesBeforeRerender).toBe(calledTimesAfterRerender);
 	});
 });

From 9af9a61b7a60d0afd8bdce7fd9305afc0e53ee0b Mon Sep 17 00:00:00 2001
From: Sergey Kozlov <dartess@mail.ru>
Date: Thu, 25 Aug 2022 23:21:26 +0300
Subject: [PATCH 15/15] add react@18 to peerDependencies of utils

---
 packages/utils/package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/utils/package.json b/packages/utils/package.json
index 1f692230b..ec8ff002f 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -22,8 +22,8 @@
 		"tsup": "^6.1.3"
 	},
 	"peerDependencies": {
-		"react": "^16.8.0 || 17.x",
-		"react-dom": "^16.8.0 || 17.x"
+		"react": "^16.8.0 || 17.x || 18.x",
+		"react-dom": "^16.8.0 || 17.x || 18.x"
 	},
 	"main": "./src/index.ts",
 	"types": "./src/index.ts",