Skip to content

Commit c667a15

Browse files
committed
Fix ESM issue with React 19
1 parent e45c669 commit c667a15

File tree

2 files changed

+30
-13
lines changed

2 files changed

+30
-13
lines changed

node_package/src/ClientSideRenderer.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/* eslint-disable max-classes-per-file */
2-
/* eslint-disable react/no-deprecated,@typescript-eslint/no-deprecated -- while we need to support React 16 */
2+
/* eslint-disable @typescript-eslint/no-deprecated -- while we need to support React 16 */
33

4-
import * as ReactDOM from 'react-dom';
54
import type { ReactElement } from 'react';
65
import type { RailsContext, RegisteredComponent, RenderFunction, Root } from './_types.ts';
76

@@ -14,6 +13,19 @@ import { debugTurbolinks } from './turbolinksUtils.ts';
1413

1514
const REACT_ON_RAILS_STORE_ATTRIBUTE = 'data-js-react-on-rails-store';
1615

16+
// Can't just import react-dom because that breaks ESM under React 19
17+
let reactDom: typeof import('react-dom');
18+
19+
const getReactDom = () => {
20+
try {
21+
// eslint-disable-next-line global-require,@typescript-eslint/no-require-imports
22+
reactDom ||= require('react-dom') as typeof import('react-dom');
23+
return reactDom;
24+
} catch (_e) {
25+
return undefined;
26+
}
27+
};
28+
1729
async function delegateToRenderer(
1830
componentObj: RegisteredComponent,
1931
props: Record<string, unknown>,
@@ -101,8 +113,7 @@ class ComponentRenderer {
101113
}
102114

103115
// Hydrate if available and was server rendered
104-
// @ts-expect-error potentially present if React 18 or greater
105-
const shouldHydrate = !!(ReactDOM.hydrate || ReactDOM.hydrateRoot) && !!domNode.innerHTML;
116+
const shouldHydrate = (supportsRootApi || !!getReactDom()?.hydrate) && !!domNode.innerHTML;
106117

107118
const reactElementOrRouterResult = createReactOutput({
108119
componentObj,
@@ -154,7 +165,7 @@ You should return a React.Component always for the client side entry point.`);
154165
}
155166

156167
try {
157-
ReactDOM.unmountComponentAtNode(domNode);
168+
getReactDom()?.unmountComponentAtNode(domNode);
158169
} catch (e: unknown) {
159170
const error = e instanceof Error ? e : new Error('Unknown error');
160171
console.info(

node_package/src/reactHydrateOrRender.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
/* eslint-disable global-require,@typescript-eslint/no-require-imports */
12
import type { ReactElement } from 'react';
2-
import * as ReactDOM from 'react-dom';
33
import type { RenderReturnType } from './_types.ts';
44
import { supportsRootApi } from './reactApis.ts';
55

@@ -8,26 +8,33 @@ type HydrateOrRenderType = (domNode: Element, reactElement: ReactElement) => Ren
88
// TODO: once React dependency is updated to >= 18, we can remove this and just
99
// import ReactDOM from 'react-dom/client';
1010
let reactDomClient: typeof import('react-dom/client');
11+
// Can't just import react-dom because that breaks ESM under React 19
12+
let reactDom: typeof import('react-dom');
1113
if (supportsRootApi) {
1214
// This will never throw an exception, but it's the way to tell Webpack the dependency is optional
1315
// https://github.com/webpack/webpack/issues/339#issuecomment-47739112
1416
// Unfortunately, it only converts the error to a warning.
1517
try {
16-
// eslint-disable-next-line global-require,@typescript-eslint/no-require-imports
1718
reactDomClient = require('react-dom/client') as typeof import('react-dom/client');
1819
} catch (_e) {
1920
// We should never get here, but if we do, we'll just use the default ReactDOM
2021
// and live with the warning.
21-
reactDomClient = ReactDOM as unknown as typeof import('react-dom/client');
22+
reactDomClient = require('react-dom') as unknown as typeof import('react-dom/client');
23+
}
24+
} else {
25+
try {
26+
reactDom = require('react-dom') as typeof import('react-dom');
27+
} catch (_e) {
28+
// Also should never happen
2229
}
2330
}
2431

25-
/* eslint-disable react/no-deprecated,@typescript-eslint/no-deprecated,@typescript-eslint/no-non-null-assertion --
32+
/* eslint-disable @typescript-eslint/no-deprecated,@typescript-eslint/no-non-null-assertion --
2633
* while we need to support React 16
2734
*/
2835
const reactHydrate: HydrateOrRenderType = supportsRootApi
2936
? reactDomClient!.hydrateRoot
30-
: (domNode, reactElement) => ReactDOM.hydrate(reactElement, domNode);
37+
: (domNode, reactElement) => reactDom!.hydrate(reactElement, domNode);
3138

3239
function reactRender(domNode: Element, reactElement: ReactElement): RenderReturnType {
3340
if (supportsRootApi) {
@@ -36,10 +43,9 @@ function reactRender(domNode: Element, reactElement: ReactElement): RenderReturn
3643
return root;
3744
}
3845

39-
// eslint-disable-next-line react/no-render-return-value
40-
return ReactDOM.render(reactElement, domNode);
46+
return reactDom!.render(reactElement, domNode);
4147
}
42-
/* eslint-enable react/no-deprecated,@typescript-eslint/no-deprecated,@typescript-eslint/no-non-null-assertion */
48+
/* eslint-enable @typescript-eslint/no-deprecated,@typescript-eslint/no-non-null-assertion */
4349

4450
export default function reactHydrateOrRender(
4551
domNode: Element,

0 commit comments

Comments
 (0)