Skip to content

Commit 3c5e74d

Browse files
committed
Require React 18
1 parent de23424 commit 3c5e74d

9 files changed

+39
-106
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,13 @@ See [Release Notes](docs/release-notes/15.0.0.md) for full details.
2929
- React Server Components Support (Pro Feature) [PR 1644](https://github.com/shakacode/react_on_rails/pull/1644) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
3030
- Improved component and store hydration performance [PR 1656](https://github.com/shakacode/react_on_rails/pull/1656) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
3131

32+
#### Removed
33+
34+
- Support for React 16 and 17. [PR 1710](https://github.com/shakacode/react_on_rails/pull/1710) by [alexeyr-ci](https://github.com/alexeyr-ci).
35+
3236
#### Breaking Changes
3337

38+
- React >=18 is now required
3439
- `ReactOnRails.reactOnRailsPageLoaded` is now an async function
3540
- `force_load` configuration now defaults to `true`
3641
- `defer_generated_component_packs` configuration now defaults to `false`

docs/guides/rails-webpacker-react-integration-options.md

+2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ const commonWebpackConfig = () => merge({}, baseClientWebpackConfig, commonOptio
8484
module.exports = commonWebpackConfig;
8585
```
8686

87+
Note this can be removed after you upgrade to React 18+.
88+
8789
---
8890

8991
## HMR and React Hot Reloading

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

+7-30
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import * as ReactDOM from 'react-dom';
21
import type { ReactElement } from 'react';
3-
import type { RailsContext, RegisteredComponent, RenderFunction, Root } from './types';
2+
import type { Root } from 'react-dom/client';
3+
import type { RailsContext, RegisteredComponent, RenderFunction } from './types';
44

55
import { getContextAndRailsContext, resetContextAndRailsContext, type Context } from './context';
66
import createReactOutput from './createReactOutput';
77
import { isServerRenderHash } from './isServerRenderResult';
88
import reactHydrateOrRender from './reactHydrateOrRender';
9-
import { supportsRootApi } from './reactApis';
109
import { debugTurbolinks } from './turbolinksUtils';
1110

1211
const REACT_ON_RAILS_STORE_ATTRIBUTE = 'data-js-react-on-rails-store';
@@ -90,9 +89,8 @@ class ComponentRenderer {
9089
return;
9190
}
9291

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;
92+
// Hydrate if the node was server rendered
93+
const shouldHydrate = !!domNode.innerHTML;
9694

9795
const reactElementOrRouterResult = createReactOutput({
9896
componentObj,
@@ -108,15 +106,12 @@ class ComponentRenderer {
108106
You returned a server side type of react-router error: ${JSON.stringify(reactElementOrRouterResult)}
109107
You should return a React.Component always for the client side entry point.`);
110108
} else {
111-
const rootOrElement = reactHydrateOrRender(
109+
this.root = reactHydrateOrRender(
112110
domNode,
113111
reactElementOrRouterResult as ReactElement,
114112
shouldHydrate,
115113
);
116114
this.state = 'rendered';
117-
if (supportsRootApi) {
118-
this.root = rootOrElement as Root;
119-
}
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
@@ -1,4 +1,5 @@
11
import type { ReactElement } from 'react';
2+
import type { Root } from 'react-dom/client';
23
import * as ClientStartup from './clientStartup';
34
import { renderOrHydrateComponent, hydrateStore } from './ClientSideRenderer';
45
import ComponentRegistry from './ComponentRegistry';
@@ -10,7 +11,6 @@ import context from './context';
1011
import type {
1112
RegisteredComponent,
1213
RenderResult,
13-
RenderReturnType,
1414
ReactComponentOrRenderFunction,
1515
AuthenticityHeaders,
1616
Store,
@@ -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

+4-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
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';
5+
import type { Root } from 'react-dom/client';
56
import type { Readable } from 'stream';
67

78
// Don't import redux just for the type definitions
@@ -158,14 +159,6 @@ export interface RenderResult {
158159
isShellReady?: boolean;
159160
}
160161

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-
169162
export interface ReactOnRails {
170163
register(components: { [id: string]: ReactComponentOrRenderFunction }): void;
171164
/** @deprecated Use registerStoreGenerators instead */
@@ -175,7 +168,7 @@ export interface ReactOnRails {
175168
getOrWaitForStore(name: string): Promise<Store>;
176169
getOrWaitForStoreGenerator(name: string): Promise<StoreGenerator>;
177170
setOptions(newOptions: { traceTurbolinks: boolean }): void;
178-
reactHydrateOrRender(domNode: Element, reactElement: ReactElement, hydrate: boolean): RenderReturnType;
171+
reactHydrateOrRender(domNode: Element, reactElement: ReactElement, hydrate: boolean): Root;
179172
reactOnRailsPageLoaded(): Promise<void>;
180173
reactOnRailsComponentLoaded(domId: string): void;
181174
reactOnRailsStoreLoaded(storeName: string): void;
@@ -185,7 +178,7 @@ export interface ReactOnRails {
185178
getStoreGenerator(name: string): StoreGenerator;
186179
setStore(name: string, store: Store): void;
187180
clearHydratedStores(): void;
188-
render(name: string, props: Record<string, string>, domNodeId: string, hydrate: boolean): RenderReturnType;
181+
render(name: string, props: Record<string, string>, domNodeId: string, hydrate: boolean): Root;
189182
getComponent(name: string): RegisteredComponent;
190183
getOrWaitForComponent(name: string): Promise<RegisteredComponent>;
191184
serverRenderReactComponent(options: RenderParams): null | string | Promise<RenderResult>;

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": [

0 commit comments

Comments
 (0)