Skip to content

Commit 639df23

Browse files
committed
Require React 18
1 parent de23424 commit 639df23

File tree

11 files changed

+47
-114
lines changed

11 files changed

+47
-114
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ See [Release Notes](docs/release-notes/15.0.0.md) for full details.
3131

3232
#### Breaking Changes
3333

34+
- React >=18 is now required
3435
- `ReactOnRails.reactOnRailsPageLoaded` is now an async function
3536
- `force_load` configuration now defaults to `true`
3637
- `defer_generated_component_packs` configuration now defaults to `false`

docs/guides/render-functions-and-railscontext.md

+9-7
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ const MyAppComponent =
2828

2929
// the props get passed again, but we ignore since we use a closure
3030
// or should we
31-
() => (
32-
<div>
33-
<p>props are: {JSON.stringify(props)}</p>
34-
<p>railsContext is: {JSON.stringify(railsContext)}</p>
35-
</div>
36-
);
31+
() =>
32+
(
33+
<div>
34+
<p>props are: {JSON.stringify(props)}</p>
35+
<p>railsContext is: {JSON.stringify(railsContext)}</p>
36+
</div>
37+
);
3738
export default MyAppComponent;
3839
```
3940

@@ -147,7 +148,8 @@ const AppComponentWithRailsContext =
147148
(props, railsContext) =>
148149
// Create a React Function Component so you can
149150
// use the React Hooks API in this React Function Component
150-
() => <AppComponent {...{ ...props, railsContext }} />;
151+
() =>
152+
<AppComponent {...{ ...props, railsContext }} />;
151153
export default AppComponentWithRailsContext;
152154
```
153155

docs/release-notes/15.0.0.md

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Major improvements to component and store hydration:
2323

2424
## Breaking Changes
2525

26+
Support for React 16 and 17 is dropped.
27+
2628
### Component Hydration Changes
2729

2830
- The `defer_generated_component_packs` and `force_load` configurations now default to `false` and `true` respectively. This means components will hydrate early without waiting for the full page load. This improves performance by eliminating unnecessary delays in hydration.

node_package/src/ClientSideRenderer.ts

+5-28
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import * as ReactDOM from 'react-dom';
21
import type { ReactElement } from 'react';
32
import type { RailsContext, RegisteredComponent, RenderFunction, Root } from './types';
43

54
import { getContextAndRailsContext, resetContextAndRailsContext, type Context } from './context';
65
import createReactOutput from './createReactOutput';
76
import { isServerRenderHash } from './isServerRenderResult';
87
import reactHydrateOrRender from './reactHydrateOrRender';
9-
import { supportsRootApi } from './reactApis';
108
import { debugTurbolinks } from './turbolinksUtils';
119

1210
const REACT_ON_RAILS_STORE_ATTRIBUTE = 'data-js-react-on-rails-store';
@@ -90,9 +88,8 @@ class ComponentRenderer {
9088
return;
9189
}
9290

93-
// Hydrate if available and was server rendered
94-
// @ts-expect-error potentially present if React 18 or greater
95-
const shouldHydrate = !!(ReactDOM.hydrate || ReactDOM.hydrateRoot) && !!domNode.innerHTML;
91+
// Hydrate if the node was server rendered
92+
const shouldHydrate = !!domNode.innerHTML;
9693

9794
const reactElementOrRouterResult = createReactOutput({
9895
componentObj,
@@ -114,9 +111,7 @@ You should return a React.Component always for the client side entry point.`);
114111
shouldHydrate,
115112
);
116113
this.state = 'rendered';
117-
if (supportsRootApi) {
118-
this.root = rootOrElement as Root;
119-
}
114+
this.root = rootOrElement as Root;
120115
}
121116
}
122117
} catch (e: unknown) {
@@ -134,26 +129,8 @@ You should return a React.Component always for the client side entry point.`);
134129
}
135130
this.state = 'unmounted';
136131

137-
if (supportsRootApi) {
138-
this.root?.unmount();
139-
this.root = undefined;
140-
} else {
141-
const domNode = document.getElementById(this.domNodeId);
142-
if (!domNode) {
143-
return;
144-
}
145-
146-
try {
147-
ReactDOM.unmountComponentAtNode(domNode);
148-
} catch (e: unknown) {
149-
const error = e instanceof Error ? e : new Error('Unknown error');
150-
console.info(
151-
`Caught error calling unmountComponentAtNode: ${error.message} for domNode`,
152-
domNode,
153-
error,
154-
);
155-
}
156-
}
132+
this.root?.unmount();
133+
this.root = undefined;
157134
}
158135

159136
waitUntilRendered(): Promise<void> {

node_package/src/ReactOnRails.client.ts

+8-15
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import context from './context';
1010
import type {
1111
RegisteredComponent,
1212
RenderResult,
13-
RenderReturnType,
1413
ReactComponentOrRenderFunction,
1514
AuthenticityHeaders,
1615
Store,
1716
StoreGenerator,
1817
} from './types';
1918
import reactHydrateOrRender from './reactHydrateOrRender';
19+
import { Root } from 'react-dom/client';
2020

2121
const ctx = context();
2222

@@ -103,13 +103,13 @@ ctx.ReactOnRails = {
103103
},
104104

105105
/**
106-
* Renders or hydrates the React element passed. In case React version is >=18 will use the root API.
106+
* Renders or hydrates the React element passed.
107107
* @param domNode
108108
* @param reactElement
109109
* @param hydrate if true will perform hydration, if false will render
110-
* @returns {Root|ReactComponent|ReactElement|null}
110+
* @returns {Root}
111111
*/
112-
reactHydrateOrRender(domNode: Element, reactElement: ReactElement, hydrate: boolean): RenderReturnType {
112+
reactHydrateOrRender(domNode: Element, reactElement: ReactElement, hydrate: boolean): Root {
113113
return reactHydrateOrRender(domNode, reactElement, hydrate);
114114
},
115115

@@ -222,26 +222,19 @@ ctx.ReactOnRails = {
222222
*
223223
* Does this:
224224
* ```js
225-
* ReactDOM.render(React.createElement(HelloWorldApp, {name: "Stranger"}),
226-
* document.getElementById('app'))
227-
* ```
228-
* under React 16/17 and
229-
* ```js
230225
* const root = ReactDOMClient.createRoot(document.getElementById('app'))
231-
* root.render(React.createElement(HelloWorldApp, {name: "Stranger"}))
226+
* root.render(React.createElement(HelloWorldApp, {name: 'Stranger'}))
232227
* return root
233228
* ```
234-
* under React 18+.
235229
*
236230
* @param name Name of your registered component
237231
* @param props Props to pass to your component
238232
* @param domNodeId
239-
* @param hydrate Pass truthy to update server rendered html. Default is falsy
240-
* @returns {Root|ReactComponent|ReactElement} Under React 18+: the created React root
233+
* @param hydrate Pass truthy to update server rendered HTML. Default is falsy
234+
* @returns {Root} The created React root
241235
* (see "What is a root?" in https://github.com/reactwg/react-18/discussions/5).
242-
* Under React 16/17: Reference to your component's backing instance or `null` for stateless components.
243236
*/
244-
render(name: string, props: Record<string, string>, domNodeId: string, hydrate: boolean): RenderReturnType {
237+
render(name: string, props: Record<string, string>, domNodeId: string, hydrate: boolean): Root {
245238
const componentObj = ComponentRegistry.get(name);
246239
const reactElement = createReactOutput({ componentObj, props, domNodeId });
247240

node_package/src/reactApis.ts

-8
This file was deleted.
+9-40
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,16 @@
11
import type { ReactElement } from 'react';
2-
import * as ReactDOM from 'react-dom';
3-
import type { RenderReturnType } from './types';
4-
import { supportsRootApi } from './reactApis';
5-
6-
type HydrateOrRenderType = (domNode: Element, reactElement: ReactElement) => RenderReturnType;
7-
8-
// TODO: once React dependency is updated to >= 18, we can remove this and just
9-
// import ReactDOM from 'react-dom/client';
10-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
11-
let reactDomClient: any;
12-
if (supportsRootApi) {
13-
// This will never throw an exception, but it's the way to tell Webpack the dependency is optional
14-
// https://github.com/webpack/webpack/issues/339#issuecomment-47739112
15-
// Unfortunately, it only converts the error to a warning.
16-
try {
17-
// eslint-disable-next-line global-require,import/no-unresolved
18-
reactDomClient = require('react-dom/client');
19-
} catch (e) {
20-
// We should never get here, but if we do, we'll just use the default ReactDOM
21-
// and live with the warning.
22-
reactDomClient = ReactDOM;
23-
}
24-
}
25-
26-
const reactHydrate: HydrateOrRenderType = supportsRootApi
27-
? reactDomClient.hydrateRoot
28-
: (domNode, reactElement) => ReactDOM.hydrate(reactElement, domNode);
29-
30-
function reactRender(domNode: Element, reactElement: ReactElement): RenderReturnType {
31-
if (supportsRootApi) {
32-
const root = reactDomClient.createRoot(domNode);
33-
root.render(reactElement);
34-
return root;
35-
}
36-
37-
// eslint-disable-next-line react/no-render-return-value
38-
return ReactDOM.render(reactElement, domNode);
39-
}
2+
import { createRoot, hydrateRoot, Root } from 'react-dom/client';
403

414
export default function reactHydrateOrRender(
425
domNode: Element,
436
reactElement: ReactElement,
447
hydrate: boolean,
45-
): RenderReturnType {
46-
return hydrate ? reactHydrate(domNode, reactElement) : reactRender(domNode, reactElement);
8+
): Root {
9+
if (hydrate) {
10+
return hydrateRoot(domNode, reactElement);
11+
}
12+
13+
const root = createRoot(domNode);
14+
root.render(reactElement);
15+
return root;
4716
}

node_package/src/types/index.ts

+1-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// eslint-disable-next-line spaced-comment
22
/// <reference types="react/experimental" />
33

4-
import type { ReactElement, ReactNode, Component, ComponentType } from 'react';
4+
import type { ReactElement, ComponentType } from 'react';
55
import type { Readable } from 'stream';
66

77
// Don't import redux just for the type definitions
@@ -158,14 +158,6 @@ export interface RenderResult {
158158
isShellReady?: boolean;
159159
}
160160

161-
// from react-dom 18
162-
export interface Root {
163-
render(children: ReactNode): void;
164-
unmount(): void;
165-
}
166-
167-
export type RenderReturnType = void | Element | Component | Root;
168-
169161
export interface ReactOnRails {
170162
register(components: { [id: string]: ReactComponentOrRenderFunction }): void;
171163
/** @deprecated Use registerStoreGenerators instead */

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@
5656
"typescript": "^5.6.2"
5757
},
5858
"peerDependencies": {
59-
"react": ">= 16",
60-
"react-dom": ">= 16",
59+
"react": ">= 18",
60+
"react-dom": ">= 18",
6161
"react-on-rails-rsc": "19.0.0"
6262
},
6363
"files": [

script/convert

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ gsub_file_content("../package.json", %r{"@arethetypeswrong/cli": "[^"]*",}, "")
2121
gsub_file_content("../package.json", %r{"@testing-library/dom": "[^"]*",}, "")
2222
gsub_file_content("../package.json", %r{"@testing-library/react": "[^"]*",}, "")
2323

24+
# Downgrade React
25+
gsub_file_content("../package.json", /"react": "[^"]*",/, '"react": "18.0.0",')
26+
gsub_file_content("../package.json", %r{"react/dom": "[^"]*",}, '"react-dom": "18.0.0",')
27+
2428
gsub_file_content("../spec/dummy/package.json", '"shakapacker": "8.0.0",', '"shakapacker": "6.6.0",')
2529

2630
gsub_file_content("../spec/dummy/config/webpack/commonWebpackConfig.js", /generateWebpackConfig(\(\))?/,

spec/dummy/client/app/startup/RouterApp.server.jsx

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { StaticRouter } from 'react-router-dom';
33

44
import routes from '../routes/routes';
55

6-
export default (props, railsContext) => () => (
7-
<StaticRouter location={railsContext.location} {...props}>
8-
{routes}
9-
</StaticRouter>
10-
);
6+
export default (props, railsContext) => () =>
7+
(
8+
<StaticRouter location={railsContext.location} {...props}>
9+
{routes}
10+
</StaticRouter>
11+
);

0 commit comments

Comments
 (0)