Skip to content

Commit fd7af60

Browse files
mrlikagregberge
authored andcommitted
feat: react 19 support
BREAKING CHANGE: Add support for React 19
1 parent 96ca3bf commit fd7af60

File tree

8 files changed

+7235
-16706
lines changed

8 files changed

+7235
-16706
lines changed

package-lock.json

Lines changed: 7099 additions & 16667 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,31 +34,35 @@
3434
"test": "jest"
3535
},
3636
"devDependencies": {
37-
"@babel/core": "^7.21.8",
38-
"@babel/eslint-parser": "^7.21.8",
39-
"@babel/preset-env": "^7.21.5",
40-
"@babel/preset-react": "^7.18.6",
41-
"@babel/preset-typescript": "^7.21.5",
42-
"@testing-library/jest-dom": "^5.16.5",
43-
"@testing-library/react": "^14.0.0",
44-
"@types/jest": "^29.5.1",
37+
"@babel/core": "^7.26.10",
38+
"@babel/eslint-parser": "^7.27.0",
39+
"@babel/preset-env": "^7.26.9",
40+
"@babel/preset-react": "^7.26.3",
41+
"@babel/preset-typescript": "^7.27.0",
42+
"@testing-library/jest-dom": "^6.6.3",
43+
"@testing-library/react": "^16.3.0",
44+
"@types/jest": "^29.5.14",
45+
"@types/react": "^19.1.2",
4546
"babel-eslint": "^10.1.0",
46-
"babel-jest": "^29.5.0",
47+
"babel-jest": "^29.7.0",
4748
"codecov": "^3.8.3",
4849
"conventional-github-releaser": "^3.1.5",
49-
"esbuild": "^0.17.19",
50+
"esbuild": "^0.25.3",
5051
"eslint": "^8.40.0",
51-
"eslint-plugin-react": "^7.32.2",
52-
"eslint-plugin-react-hooks": "^4.6.0",
53-
"jest": "^29.5.0",
54-
"jest-environment-jsdom": "^29.5.0",
55-
"prettier": "^2.8.8",
56-
"react": "^18.2.0",
57-
"react-dom": "^18.2.0",
58-
"rollup": "^3.21.7",
59-
"rollup-plugin-dts": "^5.3.0",
60-
"rollup-plugin-esbuild": "^5.0.0",
52+
"eslint-plugin-react": "^7.37.5",
53+
"eslint-plugin-react-hooks": "^5.2.0",
54+
"jest": "^29.7.0",
55+
"jest-environment-jsdom": "^29.7.0",
56+
"prettier": "^3.5.3",
57+
"react": "^19.1.0",
58+
"react-dom": "^19.1.0",
59+
"rollup": "^4.40.0",
60+
"rollup-plugin-dts": "^6.2.1",
61+
"rollup-plugin-esbuild": "^6.2.1",
6162
"standard-version": "^9.5.0",
62-
"typescript": "^5.0.4"
63+
"typescript": "^5.8.3"
64+
},
65+
"peerDependencies": {
66+
"react": ">=16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
6367
}
6468
}

rollup.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import esbuild from "rollup-plugin-esbuild";
33

44
const bundle = (config) => ({
55
...config,
6-
input: "src/index.tsx",
6+
input: "src/index.ts",
77
external: (id) => !/^[./]/.test(id),
88
});
99

src/index.test.tsx

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import * as React from "react";
22
import { render } from "@testing-library/react";
33
import { mergeRefs } from ".";
4+
import { mergeRefsReact16 } from "./mergeRefsReact16";
45

5-
test("mergeRefs", () => {
6+
test("mergeRefs React 16+", () => {
67
const Dummy = React.forwardRef(function Dummy(_, ref) {
78
React.useImperativeHandle(ref, () => "refValue");
89
return null;
910
});
1011
const refAsFunc = jest.fn();
1112
const refAsObj = { current: undefined };
1213
const Example: React.FC<{ visible: boolean }> = ({ visible }) => {
13-
return visible ? <Dummy ref={mergeRefs([refAsObj, refAsFunc])} /> : null;
14+
return visible ? (
15+
<Dummy ref={mergeRefsReact16([refAsObj, refAsFunc])} />
16+
) : null;
1417
};
1518
const { rerender } = render(<Example visible />);
1619
expect(refAsFunc).toHaveBeenCalledTimes(1);
@@ -22,6 +25,46 @@ test("mergeRefs", () => {
2225
expect(refAsObj.current).toBe(null);
2326
});
2427

28+
test("mergeRefs React 19+", () => {
29+
const Dummy = React.forwardRef(function Dummy(_, ref) {
30+
React.useImperativeHandle(ref, () => "refValue");
31+
return null;
32+
});
33+
34+
const refAsFunc = jest.fn();
35+
36+
const refAsObj = { current: undefined };
37+
38+
const refCleanup = jest.fn();
39+
const refAsFuncWithCleanup = jest.fn(() => refCleanup);
40+
41+
const Example: React.FC<{ visible: boolean }> = ({ visible }) => {
42+
return visible ? (
43+
<Dummy ref={mergeRefs([refAsObj, refAsFunc, refAsFuncWithCleanup])} />
44+
) : null;
45+
};
46+
const { rerender } = render(<Example visible />);
47+
48+
expect(refAsFunc).toHaveBeenCalledTimes(1);
49+
expect(refAsFunc).toHaveBeenCalledWith("refValue");
50+
51+
expect(refAsObj.current).toBe("refValue");
52+
53+
expect(refAsFuncWithCleanup).toHaveBeenCalledTimes(1);
54+
expect(refAsFuncWithCleanup).toHaveBeenCalledWith("refValue");
55+
expect(refCleanup).toHaveBeenCalledTimes(0);
56+
57+
rerender(<Example visible={false} />);
58+
59+
expect(refAsFunc).toHaveBeenCalledTimes(2);
60+
expect(refAsFunc).toHaveBeenCalledWith(null);
61+
62+
expect(refAsObj.current).toBe(null);
63+
64+
expect(refAsFuncWithCleanup).toHaveBeenCalledTimes(1);
65+
expect(refCleanup).toHaveBeenCalledTimes(1);
66+
});
67+
2568
test("mergeRefs with undefined and null refs", () => {
2669
const Dummy = React.forwardRef(function Dummy(_, ref) {
2770
React.useImperativeHandle(ref, () => "refValue");

src/index.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Ref, RefCallback, useMemo, version } from "react";
2+
import { mergeRefsReact16 } from "./mergeRefsReact16";
3+
import { mergeRefsReact19 } from "./mergeRefsReact19";
4+
5+
/**
6+
* Assigns a value to a ref.
7+
* @param ref The ref to assign the value to.
8+
* @param value The value to assign to the ref.
9+
* @returns The ref cleanup callback, if any.
10+
*/
11+
export function assignRef<T>(
12+
ref: Ref<T> | undefined | null,
13+
value: T | null,
14+
): ReturnType<RefCallback<T>> {
15+
if (typeof ref === "function") {
16+
return ref(value);
17+
} else if (ref) {
18+
ref.current = value;
19+
}
20+
}
21+
22+
/**
23+
* Merges multiple refs into a single one.
24+
* @param refs List of refs to merge.
25+
* @returns Merged ref.
26+
*/
27+
export const mergeRefs =
28+
parseInt(version.split(".")[0], 10) >= 19
29+
? mergeRefsReact19
30+
: mergeRefsReact16;
31+
32+
/**
33+
* Merges multiple refs into a single one and memoizes the result to avoid refs execution on each render.
34+
* @param refs List of refs to merge.
35+
* @returns Merged ref.
36+
*/
37+
export function useMergeRefs<T>(refs: (Ref<T> | undefined)[]): Ref<T> {
38+
return useMemo(() => mergeRefs(refs), refs);
39+
}

src/index.tsx

Lines changed: 0 additions & 15 deletions
This file was deleted.

src/mergeRefsReact16.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Ref } from "react";
2+
import { assignRef } from ".";
3+
4+
export function mergeRefsReact16<T>(refs: (Ref<T> | undefined)[]): Ref<T> {
5+
return (value: T | null) => {
6+
for (const ref of refs) assignRef(ref, value);
7+
};
8+
}

src/mergeRefsReact19.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Ref } from "react";
2+
import { assignRef } from ".";
3+
4+
export function mergeRefsReact19<T>(refs: (Ref<T> | undefined)[]): Ref<T> {
5+
return (value: T | null) => {
6+
const cleanups: (() => void)[] = [];
7+
8+
for (const ref of refs) {
9+
const cleanup = assignRef(ref, value);
10+
const isCleanup = typeof cleanup === "function";
11+
cleanups.push(isCleanup ? cleanup : () => assignRef(ref, null));
12+
}
13+
14+
return () => {
15+
for (const cleanup of cleanups) cleanup();
16+
};
17+
};
18+
}

0 commit comments

Comments
 (0)