Initial commit

This commit is contained in:
2025-03-07 19:22:02 +01:00
commit 4a98255d83
55743 changed files with 5280367 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
export { useIsStrictMode } from "./useIsStrictMode";
export { useDisposable } from "./useDisposable";
export type { DisposableFactory } from "./types";
+4
View File
@@ -0,0 +1,4 @@
/**
* A factory that returns the disposable instance and it's dispose function
*/
export type DisposableFactory<TInstance> = () => [TInstance, () => void];
+33
View File
@@ -0,0 +1,33 @@
import { describe, it, vi, expect } from "vitest";
import { renderHook } from "@testing-library/react";
import * as React from "react";
import { useDisposable } from "./useDisposable";
import { useStrictEffect } from "./useStrictEffect";
import { useStrictMemo } from "./useStrictMemo";
vi.mock("./useStrictMemo.ts");
vi.mock("./useStrictEffect.ts");
describe("useDisposable", () => {
describe("in strict mode", () => {
it("should not call strict effect or memo", () => {
const factory = vi.fn().mockReturnValue(["foo", vi.fn()]);
renderHook(() => useDisposable(factory, []), {
wrapper: React.StrictMode,
});
expect(useStrictEffect).not.toHaveBeenCalled();
expect(useStrictMemo).not.toHaveBeenCalled();
});
});
describe("not strict mode", () => {
it("should not call strict effect or memo", () => {
const factory = vi.fn().mockReturnValue(["foo", vi.fn()]);
renderHook(() => useDisposable(factory, []));
expect(useStrictEffect).not.toHaveBeenCalled();
expect(useStrictMemo).not.toHaveBeenCalled();
});
});
});
+101
View File
@@ -0,0 +1,101 @@
import { describe, it, vi, expect, beforeAll, afterAll } from "vitest";
import { renderHook } from "@testing-library/react";
import * as React from "react";
import { useDisposable } from "./useDisposable";
const _maybe_it = process.env.REACT_VERSION === "rc" ? it.skip : it;
describe("useDisposable", () => {
describe("in strict mode", () => {
it("should call factory once during mount", () => {
const factory = vi.fn().mockReturnValue(["foo", vi.fn()]);
renderHook(() => useDisposable(factory, []), {
wrapper: React.StrictMode,
});
expect(factory).toHaveBeenCalledTimes(1);
});
it("should not call dispose on mount", () => {
const dispose = vi.fn();
renderHook(() => useDisposable(() => ["foo", dispose], []), {
wrapper: React.StrictMode,
});
expect(dispose).toHaveBeenCalledTimes(0);
});
_maybe_it("should call dispose on unmount", () => {
const dispose = vi.fn();
const { unmount } = renderHook(
() => useDisposable(() => ["foo", dispose], []),
{
wrapper: React.StrictMode,
},
);
unmount();
expect(dispose).toHaveBeenCalledTimes(1);
});
_maybe_it(
"should call dispose and call factory if dependencies update",
() => {
const dispose = vi.fn();
const factory = vi.fn().mockReturnValue(["foo", dispose]);
let dep = "foo";
const { rerender } = renderHook(() => useDisposable(factory, [dep]), {
wrapper: React.StrictMode,
});
dep = "bar";
rerender();
expect(dispose).toHaveBeenCalledTimes(1);
expect(factory).toHaveBeenCalledTimes(2);
},
);
});
describe("not strict mode", () => {
it("should call factory once during mount", () => {
const factory = vi.fn().mockReturnValue(["foo", vi.fn()]);
renderHook(() => useDisposable(factory, []), {});
expect(factory).toHaveBeenCalledTimes(1);
});
it("should not call dispose on mount", () => {
const dispose = vi.fn();
renderHook(() => useDisposable(() => ["foo", dispose], []), {});
expect(dispose).toHaveBeenCalledTimes(0);
});
it("should call dispose on unmount", () => {
const dispose = vi.fn();
const { unmount } = renderHook(
() => useDisposable(() => ["foo", dispose], []),
{},
);
unmount();
expect(dispose).toHaveBeenCalledTimes(1);
});
it("should call dispose and call factory if dependencies update", () => {
const dispose = vi.fn();
const factory = vi.fn().mockReturnValue(["foo", dispose]);
let dep = "foo";
const { rerender } = renderHook(() => useDisposable(factory, [dep]), {});
dep = "bar";
rerender();
expect(dispose).toHaveBeenCalledTimes(1);
expect(factory).toHaveBeenCalledTimes(2);
});
});
});
+38
View File
@@ -0,0 +1,38 @@
import * as React from "react";
import type { DisposableFactory } from "./types";
import { useIsStrictMode } from "./useIsStrictMode";
import { useStrictEffect } from "./useStrictEffect";
import { useStrictMemo } from "./useStrictMemo";
/**
* Creates a disposable instance during **render time** that will
* be created once (based on dependency array) even during strict mode.
* The disposable will be disposed based on the dependency array similar to
* useEffect.
*
* ⚠️ This can only be called **once** per component
* @param factory - factory for disposable and its dispose function
* @param deps - Similar to a React dependency array
* @returns - The disposable instance
*/
export function useDisposable<TInstance>(
factory: DisposableFactory<TInstance>,
deps: any[],
) {
// In production, strict mode does not require special handling
const isStrictMode =
useIsStrictMode() && process.env.NODE_ENV !== "production";
const useMemo = isStrictMode ? useStrictMemo : React.useMemo;
const useEffect = isStrictMode ? useStrictEffect : React.useEffect;
const [disposable, dispose] = useMemo(() => factory(), deps) ?? [
null,
() => null,
];
useEffect(() => {
return dispose;
}, deps);
return disposable;
}
+35
View File
@@ -0,0 +1,35 @@
import * as React from "react";
import { renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it } from "vitest";
import { useIsStrictMode } from "./useIsStrictMode";
describe("useIsStrictMode", () => {
beforeEach(() => {
process.env.NODE_ENV = "test";
});
it("should return `true` if wrapped with StrictMode", () => {
const { result } = renderHook(useIsStrictMode, {
wrapper: React.StrictMode,
});
expect(result.current).toBe(true);
});
it("should return `false` if not wrapped with StrictMode", () => {
const { result } = renderHook(useIsStrictMode);
expect(result.current).toBe(false);
});
it("should return `false` always in production mode", () => {
process.env.NODE_ENV = "production";
const { result } = renderHook(useIsStrictMode, {
wrapper: React.StrictMode,
});
expect(result.current).toBe(false);
});
});
+71
View File
@@ -0,0 +1,71 @@
import * as React from "react";
/**
* @returns Current react fiber being rendered
*/
export const getCurrentOwner = () => {
// Note: String concatenation is used to prevent bundlers to complain with multiple versions of React
try {
// React 19
// using react internals
return (React as any)[
"".concat(
"__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE",
)
].A.getOwner();
} catch {}
try {
// React <18
// using react internals
return (React as any)[
"".concat("__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED")
].ReactCurrentOwner.current;
} catch {
if (process.env.NODE_ENV !== "production") {
console.error(
"use-disposable: failed to get current fiber, please report this bug to maintainers",
);
}
}
};
const REACT_STRICT_MODE_TYPE = /*#__PURE__*/ Symbol.for("react.strict_mode");
/**
* Traverses up the React fiber tree to find the StrictMode component.
* Note: This only detects strict mode from React >= 18
* https://github.com/reactwg/react-18/discussions/19
* @returns If strict mode is being used in the React tree
*/
export const useIsStrictMode = (): boolean => {
// This check violates Rules of Hooks, but "process.env.NODE_ENV" does not change in bundle
// or during application lifecycle
if (process.env.NODE_ENV === "production") {
return false;
}
const isStrictMode = React.useRef<boolean | undefined>(undefined);
const reactMajorVersion = React.useMemo(() => {
return Number(React.version.split(".")[0]);
}, [React.version]);
if (isNaN(reactMajorVersion) || reactMajorVersion < 18) {
return false;
}
if (isStrictMode.current === undefined) {
let currentOwner = getCurrentOwner();
while (currentOwner && currentOwner.return) {
currentOwner = currentOwner.return;
if (
currentOwner.type === REACT_STRICT_MODE_TYPE ||
currentOwner.elementType === REACT_STRICT_MODE_TYPE
) {
isStrictMode.current = true;
}
}
}
return !!isStrictMode.current;
};
+42
View File
@@ -0,0 +1,42 @@
import { describe, it, vi, expect } from "vitest";
import { renderHook } from "@testing-library/react";
import * as React from "react";
import { useStrictEffect } from "./useStrictEffect";
describe("useStrictEffect", () => {
it("should only dispose on unmount in strict mode", () => {
const dispose = vi.fn();
const { unmount } = renderHook(
() =>
useStrictEffect(() => {
return dispose;
}, []),
{ wrapper: React.StrictMode },
);
expect(dispose).toHaveBeenCalledTimes(0);
unmount();
expect(dispose).toHaveBeenCalledTimes(1);
});
it("should dispose when dependency array changes", () => {
const dispose = vi.fn();
let dep = "foo";
const { rerender } = renderHook(
() =>
useStrictEffect(() => {
return dispose;
}, [dep]),
{ wrapper: React.StrictMode },
);
expect(dispose).toHaveBeenCalledTimes(0);
dep = "bar";
rerender();
expect(dispose).toHaveBeenCalledTimes(1);
});
});
+23
View File
@@ -0,0 +1,23 @@
import * as React from "react";
import { getCurrentOwner } from "./useIsStrictMode";
// we know strict mode will render useMemo facory twice
// keep a weak set to detect when the second render happens
const effectSet = new WeakSet();
export function useStrictEffect(
effect: () => () => void,
deps: React.DependencyList | undefined,
) {
const currentOwner = getCurrentOwner();
React.useEffect(() => {
if (!effectSet.has(currentOwner)) {
effectSet.add(currentOwner);
effect();
return;
}
const dispose = effect();
return dispose;
}, deps);
}
+28
View File
@@ -0,0 +1,28 @@
import { describe, it, vi, expect } from "vitest";
import { renderHook } from "@testing-library/react";
import * as React from "react";
import { useStrictMemo } from "./useStrictMemo";
describe("useStrictMemo", () => {
it("should not call factory twice on mount in strict mode", () => {
const factory = vi.fn();
renderHook(() => useStrictMemo(factory, []), {
wrapper: React.StrictMode,
});
expect(factory).toHaveBeenCalledTimes(1);
});
it("should call factory if dependencies update", () => {
const factory = vi.fn();
let dep = "foo";
const { rerender } = renderHook(() => useStrictMemo(factory, [dep]), {
wrapper: React.StrictMode,
});
dep = "bar";
rerender();
expect(factory).toHaveBeenCalledTimes(2);
});
});
+21
View File
@@ -0,0 +1,21 @@
import * as React from "react";
import { getCurrentOwner } from "./useIsStrictMode";
// we know strict mode will render useMemo facory twice
// keep a weak set to detect when the second render happens
const memoSet = new WeakSet();
export function useStrictMemo<TMemoized>(
factory: () => any,
deps: React.DependencyList | undefined,
): TMemoized | null {
return React.useMemo(() => {
const currentOwner = getCurrentOwner();
if (!memoSet.has(currentOwner)) {
memoSet.add(currentOwner);
return null;
}
return factory();
}, deps);
}