Skip to content

Commit 99fb601

Browse files
authored
Merge pull request #46 from WTW-IM/fix-react-dom
2 parents 4146244 + 954ceeb commit 99fb601

File tree

4 files changed

+74
-52
lines changed

4 files changed

+74
-52
lines changed

src/ReactHTMLElement.ts

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
1-
import { version as reactVersion } from 'react';
2-
import ReactDOM from 'react-dom';
3-
import type { Root, createRoot as createRootOriginal } from 'react-dom/client';
1+
import type ReactDOM from 'react-dom';
2+
import type { Root } from 'react-dom/client';
3+
import { getCreateRoot } from './react-dom-client';
44

55
type Renderable = Parameters<ReactDOM.Renderer>[0][number];
6-
7-
const [, major] = /^(\d+)\.\d+\.\d+$/.exec(reactVersion) || [undefined, '16'];
8-
const reactMajor = Number(major);
9-
10-
const isPreEighteen = reactMajor < 18;
11-
const REACT_DOM_CLIENT_IMPORT = isPreEighteen
12-
? './react-dom-client-polyfill'
13-
: 'react-dom/client';
6+
type ReactHTMLElementDOMRoot = Pick<Root, 'render' | 'unmount'>;
7+
8+
const awaitValue = <T>(awaiter: () => T): Promise<T> => new Promise((resolve) => {
9+
const result = awaiter();
10+
if (result) {
11+
resolve(result);
12+
} else {
13+
setTimeout(() => resolve(awaitValue(awaiter)), 100);
14+
}
15+
});
1416

1517
class ReactHTMLElement extends HTMLElement {
1618
private _initialized?: boolean;
1719

1820
private _mountPoint?: Element;
1921

20-
private _root?: Root;
22+
private _root?: ReactHTMLElementDOMRoot;
23+
24+
private _awaitingRoot = false;
2125

2226
private getShadowRoot(): ShadowRoot {
2327
return this.shadowRoot || this.attachShadow({ mode: 'open' });
@@ -49,27 +53,21 @@ class ReactHTMLElement extends HTMLElement {
4953
this._mountPoint = mount;
5054
}
5155

52-
async root(): Promise<Root> {
56+
async root(): Promise<ReactHTMLElementDOMRoot> {
57+
if (this._awaitingRoot) {
58+
await awaitValue(() => this._root);
59+
}
5360
if (this._root) return this._root;
5461

55-
const { createRoot } = (await import(
56-
/* webpackExports: ['createRoot'] */
57-
`${REACT_DOM_CLIENT_IMPORT}`
58-
)) as {
59-
createRoot: typeof createRootOriginal;
60-
};
61-
this._root = createRoot(this.mountPoint);
62+
this._awaitingRoot = true;
63+
this._root = (await getCreateRoot())(this.mountPoint);
64+
this._awaitingRoot = false;
6265
return this._root;
6366
}
6467

6568
render(app: Renderable): void {
6669
if (!this.isConnected) return;
6770

68-
if (isPreEighteen) {
69-
ReactDOM.render(app, this.mountPoint);
70-
return;
71-
}
72-
7371
void this.renderRoot(app);
7472
}
7573

@@ -81,11 +79,6 @@ class ReactHTMLElement extends HTMLElement {
8179
disconnectedCallback(): void {
8280
if (!this._mountPoint) return;
8381

84-
if (isPreEighteen) {
85-
ReactDOM.unmountComponentAtNode(this._mountPoint);
86-
return;
87-
}
88-
8982
this._root?.unmount();
9083
}
9184

src/__tests__/ReactHtmlElement.tests.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import React, { useState, useEffect } from 'react';
2-
import { findByText, waitFor, queryByTestId } from '@testing-library/dom';
2+
import {
3+
findByText,
4+
waitFor,
5+
queryByTestId,
6+
findByTestId,
7+
} from '@testing-library/dom';
38
import '@testing-library/jest-dom/extend-expect';
49
import ReactHTMLElement from '../ReactHTMLElement';
510

611
function ReactTest({ onUnmount = (): void => undefined }): React.ReactElement {
712
const [increment, setIncrement] = useState(0);
8-
useEffect(() => (): void => onUnmount());
13+
useEffect(() => (): void => onUnmount(), []);
914
return (
1015
<div data-testid="container">
1116
<button
@@ -39,9 +44,10 @@ async function getDocument(
3944
testElement.onUnmount = onUnmount;
4045
document.body.appendChild(testElement);
4146
await waitFor(() => expect(testElement.shadowRoot).toBeTruthy());
42-
return testElement.shadowRoot?.querySelector(
43-
'div[data-testid=container]'
44-
) as HTMLElement;
47+
return findByTestId(
48+
(testElement.shadowRoot as unknown) as HTMLElement,
49+
'container'
50+
);
4551
}
4652

4753
it('renders interactable react', async () => {

src/react-dom-client-polyfill.ts

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

src/react-dom-client.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import * as ReactDOM from 'react-dom';
2+
import type { createRoot as createRootOriginal } from 'react-dom/client';
3+
4+
type ReactDOM18 = ReactDOMOriginal & {
5+
createRoot?: CreateRoot;
6+
};
7+
8+
type CreateRoot = typeof createRootOriginal;
9+
type CreateRootParams = Parameters<CreateRoot>;
10+
type ReactDOMOriginal = typeof ReactDOM;
11+
type RendererProps = Parameters<typeof ReactDOM['render']>;
12+
13+
const createRootFake = (container: CreateRootParams[0]) => {
14+
const newRoot = {
15+
render: (element: RendererProps[0]) => {
16+
ReactDOM.render(element, container);
17+
},
18+
unmount: () => {
19+
ReactDOM.unmountComponentAtNode(container);
20+
},
21+
};
22+
return newRoot;
23+
};
24+
25+
let checkedFor18 = false;
26+
let MaybeReactDOM18: ReactDOM18;
27+
// eslint-disable-next-line import/prefer-default-export
28+
export const getCreateRoot = async () => {
29+
if (!checkedFor18) {
30+
try {
31+
// eslint-disable-next-line global-require,@typescript-eslint/no-var-requires
32+
MaybeReactDOM18 = (await require('react-dom/client')) as ReactDOM18;
33+
} catch {
34+
MaybeReactDOM18 = ReactDOM as ReactDOM18;
35+
}
36+
checkedFor18 = true;
37+
}
38+
const { createRoot = createRootFake } = MaybeReactDOM18;
39+
return createRoot;
40+
};

0 commit comments

Comments
 (0)