react-on-rails 16.2.0-test.6 → 16.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ClientRenderer.js +38 -2
- package/lib/pageLifecycle.js +10 -1
- package/lib/reactApis.cjs +21 -8
- package/lib/serverRenderReactComponent.js +1 -0
- package/lib/types/index.d.ts +2 -2
- package/package.json +2 -8
package/lib/ClientRenderer.js
CHANGED
|
@@ -51,11 +51,48 @@ function renderElement(el, railsContext) {
|
|
|
51
51
|
try {
|
|
52
52
|
const domNode = document.getElementById(domNodeId);
|
|
53
53
|
if (domNode) {
|
|
54
|
+
// Check if this component was already rendered by a previous call
|
|
55
|
+
// This prevents hydration errors when reactOnRailsPageLoaded() is called multiple times
|
|
56
|
+
// (e.g., for asynchronously loaded content)
|
|
57
|
+
const existing = renderedRoots.get(domNodeId);
|
|
58
|
+
if (existing) {
|
|
59
|
+
// Only skip if it's the exact same DOM node and it's still connected to the document.
|
|
60
|
+
// If the node was replaced (e.g., via innerHTML or Turbo), we need to unmount the old
|
|
61
|
+
// root and re-render to the new node to prevent memory leaks and ensure rendering works.
|
|
62
|
+
const sameNode = existing.domNode === domNode && existing.domNode.isConnected;
|
|
63
|
+
if (sameNode) {
|
|
64
|
+
if (trace) {
|
|
65
|
+
console.log(`Skipping already rendered component: ${name} (dom id: ${domNodeId})`);
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// DOM node was replaced (e.g., via async HTML injection) - clean up the old root
|
|
70
|
+
try {
|
|
71
|
+
if (supportsRootApi &&
|
|
72
|
+
existing.root &&
|
|
73
|
+
typeof existing.root === 'object' &&
|
|
74
|
+
'unmount' in existing.root) {
|
|
75
|
+
existing.root.unmount();
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
unmountComponentAtNode(existing.domNode);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (unmountError) {
|
|
82
|
+
// Ignore unmount errors for replaced nodes
|
|
83
|
+
if (trace) {
|
|
84
|
+
console.log(`Error unmounting replaced component: ${name}`, unmountError);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
renderedRoots.delete(domNodeId);
|
|
88
|
+
}
|
|
54
89
|
const componentObj = ComponentRegistry.get(name);
|
|
55
90
|
if (delegateToRenderer(componentObj, props, railsContext, domNodeId, trace)) {
|
|
56
91
|
return;
|
|
57
92
|
}
|
|
58
|
-
// Hydrate if
|
|
93
|
+
// Hydrate if the DOM node has content (server-rendered HTML)
|
|
94
|
+
// Since we skip already-rendered components above, this check now correctly
|
|
95
|
+
// identifies only server-rendered content, not previously client-rendered content
|
|
59
96
|
const shouldHydrate = !!domNode.innerHTML;
|
|
60
97
|
const reactElementOrRouterResult = createReactOutput({
|
|
61
98
|
componentObj,
|
|
@@ -140,7 +177,6 @@ function unmountAllComponents() {
|
|
|
140
177
|
}
|
|
141
178
|
else {
|
|
142
179
|
// React 16-17 legacy API
|
|
143
|
-
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
144
180
|
unmountComponentAtNode(domNode);
|
|
145
181
|
}
|
|
146
182
|
}
|
package/lib/pageLifecycle.js
CHANGED
|
@@ -50,7 +50,16 @@ function initializePageEventListeners() {
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
isPageLifecycleInitialized = true;
|
|
53
|
-
|
|
53
|
+
// Important: replacing this condition with `document.readyState !== 'loading'` is not valid
|
|
54
|
+
// As the core ReactOnRails needs to ensure that all component bundles are loaded and executed before hydrating them
|
|
55
|
+
// If the `document.readyState === 'interactive'`, it doesn't guarantee that deferred scripts are executed
|
|
56
|
+
// the `readyState` can be `'interactive'` while the deferred scripts are still being executed
|
|
57
|
+
// Which will lead to the error `"Could not find component registered with name <component name>"`
|
|
58
|
+
// It will happen if this line is reached before the component chunk is executed on browser and reached the line
|
|
59
|
+
// ReactOnRails.register({ Component });
|
|
60
|
+
// ReactOnRailsPro is resellient against that type of race conditions, but it won't wait for that state anyway
|
|
61
|
+
// As it immediately hydrates the components at the page as soon as its html and bundle is loaded on the browser
|
|
62
|
+
if (document.readyState === 'complete') {
|
|
54
63
|
setupPageNavigationListeners();
|
|
55
64
|
}
|
|
56
65
|
else {
|
package/lib/reactApis.cjs
CHANGED
|
@@ -25,25 +25,38 @@ if (exports.supportsRootApi) {
|
|
|
25
25
|
reactDomClient = ReactDOM;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
// Cast ReactDOM to include legacy APIs for React 16/17 compatibility
|
|
29
|
+
// These methods exist at runtime but are removed from @types/react-dom@19
|
|
30
|
+
const legacyReactDOM = ReactDOM;
|
|
31
|
+
// Validate legacy APIs exist at runtime when needed (React < 18)
|
|
32
|
+
if (!exports.supportsRootApi) {
|
|
33
|
+
if (typeof legacyReactDOM.hydrate !== 'function') {
|
|
34
|
+
throw new Error('React legacy hydrate API not available. Expected React 16/17.');
|
|
35
|
+
}
|
|
36
|
+
if (typeof legacyReactDOM.render !== 'function') {
|
|
37
|
+
throw new Error('React legacy render API not available. Expected React 16/17.');
|
|
38
|
+
}
|
|
39
|
+
if (typeof legacyReactDOM.unmountComponentAtNode !== 'function') {
|
|
40
|
+
throw new Error('React legacy unmountComponentAtNode API not available. Expected React 16/17.');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- reactDomClient is always defined when supportsRootApi is true */
|
|
31
44
|
exports.reactHydrate = exports.supportsRootApi
|
|
32
45
|
? reactDomClient.hydrateRoot
|
|
33
|
-
: (domNode, reactElement) =>
|
|
46
|
+
: (domNode, reactElement) => legacyReactDOM.hydrate(reactElement, domNode);
|
|
34
47
|
function reactRender(domNode, reactElement) {
|
|
35
48
|
if (exports.supportsRootApi) {
|
|
36
49
|
const root = reactDomClient.createRoot(domNode);
|
|
37
50
|
root.render(reactElement);
|
|
38
51
|
return root;
|
|
39
52
|
}
|
|
40
|
-
|
|
41
|
-
return ReactDOM.render(reactElement, domNode);
|
|
53
|
+
return legacyReactDOM.render(reactElement, domNode);
|
|
42
54
|
}
|
|
43
55
|
exports.unmountComponentAtNode = exports.supportsRootApi
|
|
44
56
|
? // not used if we use root API
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
58
|
+
(_container) => false
|
|
59
|
+
: (container) => legacyReactDOM.unmountComponentAtNode(container);
|
|
47
60
|
const ensureReactUseAvailable = () => {
|
|
48
61
|
if (!('use' in React) || typeof React.use !== 'function') {
|
|
49
62
|
throw new Error('React.use is not defined. Please ensure you are using React 19 to use server components.');
|
|
@@ -49,6 +49,7 @@ function processPromise(result, renderingReturnsPromises) {
|
|
|
49
49
|
if (isValidElement(promiseResult)) {
|
|
50
50
|
return processReactElement(promiseResult);
|
|
51
51
|
}
|
|
52
|
+
// promiseResult is string | ServerRenderHashRenderedHtml (both are FinalHtmlResult)
|
|
52
53
|
return promiseResult;
|
|
53
54
|
});
|
|
54
55
|
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -68,8 +68,8 @@ interface ServerRenderResult {
|
|
|
68
68
|
routeError?: Error;
|
|
69
69
|
error?: Error;
|
|
70
70
|
}
|
|
71
|
-
type CreateReactOutputSyncResult = ServerRenderResult | ReactElement
|
|
72
|
-
type CreateReactOutputAsyncResult = Promise<string | ServerRenderHashRenderedHtml | ReactElement
|
|
71
|
+
type CreateReactOutputSyncResult = ServerRenderResult | ReactElement;
|
|
72
|
+
type CreateReactOutputAsyncResult = Promise<string | ServerRenderHashRenderedHtml | ReactElement>;
|
|
73
73
|
type CreateReactOutputResult = CreateReactOutputSyncResult | CreateReactOutputAsyncResult;
|
|
74
74
|
type RenderFunctionSyncResult = ReactComponent | ServerRenderResult;
|
|
75
75
|
type RenderFunctionAsyncResult = Promise<string | ServerRenderHashRenderedHtml | ReactComponent>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-on-rails",
|
|
3
|
-
"version": "16.2.0
|
|
3
|
+
"version": "16.2.0",
|
|
4
4
|
"description": "react-on-rails JavaScript for react_on_rails Ruby gem",
|
|
5
5
|
"main": "lib/ReactOnRails.full.js",
|
|
6
6
|
"type": "module",
|
|
@@ -48,13 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"react": ">= 16",
|
|
51
|
-
"react-dom": ">= 16"
|
|
52
|
-
"react-on-rails-rsc": "19.0.2"
|
|
53
|
-
},
|
|
54
|
-
"peerDependenciesMeta": {
|
|
55
|
-
"react-on-rails-rsc": {
|
|
56
|
-
"optional": true
|
|
57
|
-
}
|
|
51
|
+
"react-dom": ">= 16"
|
|
58
52
|
},
|
|
59
53
|
"files": [
|
|
60
54
|
"lib/**/*.js",
|