Skip to content

Commit 70f9996

Browse files
authored
fix(useColumnsAutoSize): support React 17 by lazy-loading react-dom/client (#154)
1 parent 641ef9d commit 70f9996

2 files changed

Lines changed: 99 additions & 8 deletions

File tree

src/hooks/useColumnsAutoSize/hooks/useMeasureCellWidth.tsx

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import * as React from 'react';
22

3-
import {createRoot} from 'react-dom/client';
4-
import type {Root} from 'react-dom/client';
5-
63
import {cellDefaultWidth, headerDefaultWidth} from '../constants';
4+
import {createMeasureRoot} from '../utils/createMeasureRoot';
5+
import type {MeasureRoot} from '../utils/createMeasureRoot';
76
import {renderElementForMeasure as defaultRenderElementForMeasure} from '../utils/renderElementForMeasure';
87

98
export type UseMeasureCellWidthProps = {
@@ -13,29 +12,61 @@ export type UseMeasureCellWidthProps = {
1312
export function useMeasureCellWidth({
1413
renderElementForMeasure = defaultRenderElementForMeasure,
1514
}: UseMeasureCellWidthProps) {
16-
const rootRef = React.useRef<Root | null>(null);
15+
const rootRef = React.useRef<MeasureRoot | null>(null);
16+
const rootPromiseRef = React.useRef<Promise<MeasureRoot> | null>(null);
17+
const isUnmountedRef = React.useRef(false);
1718
const measureContainerRef = React.useRef<HTMLDivElement | null>(null);
1819
const lastMeasuredElementRef = React.useRef<{
1920
element: React.ReactNode;
2021
width: number;
2122
} | null>(null);
2223

2324
React.useEffect(() => {
25+
isUnmountedRef.current = false;
26+
2427
return () => {
28+
isUnmountedRef.current = true;
29+
2530
if (rootRef.current) {
2631
rootRef.current.unmount();
2732
rootRef.current = null;
2833
}
2934

35+
rootPromiseRef.current = null;
36+
3037
if (measureContainerRef.current) {
3138
document.body.removeChild(measureContainerRef.current);
3239
measureContainerRef.current = null;
3340
}
3441
};
3542
}, []);
3643

44+
const ensureRoot = React.useCallback((container: HTMLElement) => {
45+
if (rootRef.current) {
46+
return Promise.resolve(rootRef.current);
47+
}
48+
49+
if (!rootPromiseRef.current) {
50+
rootPromiseRef.current = createMeasureRoot(container).then((root) => {
51+
// The hook may have unmounted while the React 18 client entry
52+
// was being resolved; tear the orphan root down immediately.
53+
if (isUnmountedRef.current) {
54+
root.unmount();
55+
56+
return root;
57+
}
58+
59+
rootRef.current = root;
60+
61+
return root;
62+
});
63+
}
64+
65+
return rootPromiseRef.current;
66+
}, []);
67+
3768
return React.useCallback(
38-
(element: React.ReactNode, cellType: 'header' | 'cell' = 'cell') => {
69+
async (element: React.ReactNode, cellType: 'header' | 'cell' = 'cell') => {
3970
if (element === null || element === undefined) {
4071
return 0;
4172
}
@@ -52,7 +83,6 @@ export function useMeasureCellWidth({
5283

5384
document.body.appendChild(container);
5485
measureContainerRef.current = container;
55-
rootRef.current = createRoot(container);
5686
}
5787

5888
if (
@@ -94,7 +124,9 @@ export function useMeasureCellWidth({
94124
}
95125

96126
try {
97-
rootRef.current!.render(renderElementForMeasure(element));
127+
const root = await ensureRoot(measureContainerRef.current);
128+
129+
root.render(renderElementForMeasure(element));
98130

99131
return new Promise<number>((resolve) => {
100132
setTimeout(() => {
@@ -129,6 +161,6 @@ export function useMeasureCellWidth({
129161
return defaultWidth;
130162
}
131163
},
132-
[renderElementForMeasure],
164+
[ensureRoot, renderElementForMeasure],
133165
);
134166
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type * as React from 'react';
2+
3+
import * as ReactDOM from 'react-dom';
4+
5+
export interface MeasureRoot {
6+
render(element: React.ReactElement): void;
7+
unmount(): void;
8+
}
9+
10+
type CreateRoot = (container: Element | DocumentFragment) => {
11+
render(children: React.ReactNode): void;
12+
unmount(): void;
13+
};
14+
15+
type LegacyReactDOM = {
16+
render(element: React.ReactElement, container: Element): void;
17+
unmountComponentAtNode(container: Element): void;
18+
};
19+
20+
let cachedCreateRoot: CreateRoot | null | undefined;
21+
22+
async function resolveCreateRoot(): Promise<CreateRoot | null> {
23+
if (cachedCreateRoot !== undefined) {
24+
return cachedCreateRoot;
25+
}
26+
27+
try {
28+
// `react-dom/client` exists only in React 18+. In React 17 this subpath
29+
// is absent, so the dynamic import rejects and we fall back to the
30+
// legacy API below.
31+
const mod = (await import('react-dom/client')) as {createRoot?: CreateRoot};
32+
33+
cachedCreateRoot = mod.createRoot ?? null;
34+
} catch {
35+
cachedCreateRoot = null;
36+
}
37+
38+
return cachedCreateRoot;
39+
}
40+
41+
export async function createMeasureRoot(container: HTMLElement): Promise<MeasureRoot> {
42+
const createRoot = await resolveCreateRoot();
43+
44+
if (createRoot) {
45+
const root = createRoot(container);
46+
47+
return {
48+
render: (element) => root.render(element),
49+
unmount: () => root.unmount(),
50+
};
51+
}
52+
53+
const legacy = ReactDOM as unknown as LegacyReactDOM;
54+
55+
return {
56+
render: (element) => legacy.render(element, container),
57+
unmount: () => legacy.unmountComponentAtNode(container),
58+
};
59+
}

0 commit comments

Comments
 (0)