Skip to content

Commit ed9c5d8

Browse files
authored
Test React on Rails 17.0.0.rc.7 (#775)
* Test React on Rails 17 rc7 * Fix RSC error recovery demo * Tighten RSC error recovery flow
1 parent 196504a commit ed9c5d8

8 files changed

Lines changed: 135 additions & 72 deletions

File tree

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
66
ruby "3.4.6"
77

88
gem "cpflow", "5.1.1", require: false
9-
gem "react_on_rails_pro", "17.0.0.rc.6"
9+
gem "react_on_rails_pro", "17.0.0.rc.7"
1010
gem "shakapacker", "10.2.0"
1111

1212
# Bundle edge Rails instead: gem "rails", github: "rails/rails"

Gemfile.lock

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ GEM
7878
addressable (2.9.0)
7979
public_suffix (>= 2.0.2, < 8.0)
8080
ast (2.4.3)
81-
async (2.39.0)
81+
async (2.42.0)
8282
console (~> 1.29)
8383
fiber-annotation
8484
io-event (~> 1.11)
@@ -191,7 +191,7 @@ GEM
191191
interception (0.5)
192192
io-console (0.8.2)
193193
io-endpoint (0.17.2)
194-
io-event (1.16.2)
194+
io-event (1.19.1)
195195
io-stream (0.13.1)
196196
irb (1.18.0)
197197
pp (>= 0.6.0)
@@ -346,22 +346,21 @@ GEM
346346
erb
347347
psych (>= 4.0.0)
348348
tsort
349-
react_on_rails (17.0.0.rc.6)
349+
react_on_rails (17.0.0.rc.7)
350350
addressable
351351
connection_pool
352352
execjs (~> 2.5)
353353
rails (>= 5.2)
354354
rainbow (~> 3.0)
355355
shakapacker (>= 6.0)
356-
react_on_rails_pro (17.0.0.rc.6)
357-
addressable
356+
react_on_rails_pro (17.0.0.rc.7)
358357
async (>= 2.29)
359358
async-http (~> 0.95)
360359
execjs (~> 2.9)
361360
io-endpoint (~> 0.17.0)
362361
jwt (>= 2.5, < 4)
363-
rainbow
364-
react_on_rails (= 17.0.0.rc.6)
362+
nokogiri (>= 1.12, < 2)
363+
react_on_rails (= 17.0.0.rc.7)
365364
redcarpet (3.6.0)
366365
redis (5.3.0)
367366
redis-client (>= 0.22.0)
@@ -549,7 +548,7 @@ DEPENDENCIES
549548
rails-html-sanitizer
550549
rails_best_practices
551550
rainbow
552-
react_on_rails_pro (= 17.0.0.rc.6)
551+
react_on_rails_pro (= 17.0.0.rc.7)
553552
redcarpet
554553
redis (~> 5.0)
555554
rspec-rails (~> 6.0.0)

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,11 @@ assets_bundler: rspack
184184

185185
### Version Targets
186186

187-
- `react_on_rails_pro` gem: `17.0.0.rc.6`
188-
- `react-on-rails-pro` npm package: `17.0.0-rc.6`
189-
- `react-on-rails-pro-node-renderer` npm package: `17.0.0-rc.6`
190-
- `react-on-rails-rsc` npm package: `19.2.0`
191-
- `shakapacker` gem/npm package: `10.1.0`
187+
- `react_on_rails_pro` gem: `17.0.0.rc.7`
188+
- `react-on-rails-pro` npm package: `17.0.0-rc.7`
189+
- `react-on-rails-pro-node-renderer` npm package: `17.0.0-rc.7`
190+
- `react-on-rails-rsc` npm package: `19.2.1-rc.0`
191+
- `shakapacker` gem/npm package: `10.2.0`
192192
- `@rspack/core` and `@rspack/cli`: `2.0.0-beta.7`
193193
- `react`: `~19.2.7` (minimum for React Server Components)
194194

client/app/bundles/server-components/components/LiveActivityRefresher.jsx

Lines changed: 98 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
'use client';
22

3-
import React, { useState, Suspense } from 'react';
3+
import React, { Suspense, useRef, useState } from 'react';
44
import { ErrorBoundary } from 'react-error-boundary';
55
import RSCRoute from 'react-on-rails-pro/RSCRoute';
66
import { useRSC } from 'react-on-rails-pro/RSCProvider';
7+
import { ServerComponentFetchError } from 'react-on-rails-pro/ServerComponentFetchError';
78

89
// Same shape and dimensions as the rendered LiveActivity card. Local Suspense
910
// fallback prevents the RSCRoute suspension from bubbling to an outer
@@ -23,33 +24,102 @@ function ActivityCardSkeleton() {
2324
);
2425
}
2526

27+
function ActivityErrorFallback({ error, onRetry }) {
28+
return (
29+
<div className="bg-rose-50 border border-rose-200 rounded-lg p-4">
30+
<p className="text-rose-700 font-semibold mb-1">Server component fetch failed</p>
31+
<p className="text-rose-600 text-sm font-mono mb-3">{error.message}</p>
32+
<button
33+
type="button"
34+
onClick={onRetry}
35+
className="px-3 py-1.5 text-sm bg-rose-600 text-white rounded hover:bg-rose-700"
36+
>
37+
Retry
38+
</button>
39+
</div>
40+
);
41+
}
42+
43+
function toServerComponentFetchError(error, componentProps) {
44+
if (error instanceof ServerComponentFetchError) {
45+
return error;
46+
}
47+
48+
const originalError = error instanceof Error ? error : new Error(String(error));
49+
return new ServerComponentFetchError(originalError.message, 'LiveActivity', componentProps, originalError);
50+
}
51+
52+
function ThrowActivityError({ error }) {
53+
throw error;
54+
}
55+
2656
function LiveActivityRefresher() {
27-
const [refreshKey, setRefreshKey] = useState(0);
28-
const [simulateError, setSimulateError] = useState(false);
57+
const [refreshCount, setRefreshCount] = useState(0);
58+
const [routeRefreshKey, setRouteRefreshKey] = useState(0);
59+
const [fetchError, setFetchError] = useState(null);
60+
const latestRequestIdRef = useRef(0);
2961
const { refetchComponent } = useRSC();
3062

63+
const nextRequestId = () => {
64+
latestRequestIdRef.current += 1;
65+
return latestRequestIdRef.current;
66+
};
67+
68+
const refetchLiveActivity = (componentProps) =>
69+
refetchComponent('LiveActivity', componentProps, true).then((payload) => {
70+
if (payload instanceof Error) {
71+
throw payload;
72+
}
73+
});
74+
3175
const handleRefresh = () => {
32-
setSimulateError(false);
33-
setRefreshKey((k) => k + 1);
76+
const nextKey = refreshCount + 1;
77+
78+
nextRequestId();
79+
setFetchError(null);
80+
setRefreshCount(nextKey);
81+
setRouteRefreshKey(nextKey);
3482
};
3583

3684
const handleSimulateError = () => {
37-
setSimulateError(true);
38-
setRefreshKey((k) => k + 1);
85+
const requestId = nextRequestId();
86+
const nextKey = refreshCount + 1;
87+
const errorProps = { simulateError: true, refreshKey: nextKey };
88+
89+
setFetchError(null);
90+
setRefreshCount(nextKey);
91+
refetchLiveActivity(errorProps).catch((error) => {
92+
if (latestRequestIdRef.current === requestId) {
93+
setFetchError(toServerComponentFetchError(error, errorProps));
94+
}
95+
});
3996
};
4097

41-
// refetchComponent primes the cache with corrected props before resetting
42-
// the boundary, so the post-reset render hits cache instead of re-fetching.
43-
const buildRetry = (resetErrorBoundary) => () => {
44-
const newKey = refreshKey + 1;
45-
setSimulateError(false);
46-
setRefreshKey(newKey);
47-
refetchComponent('LiveActivity', { simulateError: false, refreshKey: newKey })
48-
// eslint-disable-next-line no-console
49-
.catch((err) => console.error('Retry refetch failed:', err))
50-
.finally(() => resetErrorBoundary());
98+
const buildBoundaryRetry = (resetErrorBoundary) => () => {
99+
const requestId = nextRequestId();
100+
const newKey = refreshCount + 1;
101+
const correctedProps = { simulateError: false, refreshKey: newKey };
102+
103+
setFetchError(null);
104+
setRefreshCount(newKey);
105+
refetchLiveActivity(correctedProps)
106+
.then(() => {
107+
if (latestRequestIdRef.current === requestId) {
108+
setRouteRefreshKey(newKey);
109+
resetErrorBoundary();
110+
}
111+
})
112+
.catch((err) => {
113+
if (latestRequestIdRef.current === requestId) {
114+
// eslint-disable-next-line no-console
115+
console.error('Retry refetch failed:', err);
116+
setFetchError(toServerComponentFetchError(err, correctedProps));
117+
}
118+
});
51119
};
52120

121+
const componentProps = { simulateError: false, refreshKey: routeRefreshKey };
122+
53123
return (
54124
<div className="space-y-3">
55125
<div className="flex items-center gap-2">
@@ -67,30 +137,24 @@ function LiveActivityRefresher() {
67137
>
68138
Simulate Error
69139
</button>
70-
<span className="text-xs text-slate-500 ml-2">Refresh count: {refreshKey}</span>
140+
<span className="text-xs text-slate-500 ml-2">Refresh count: {refreshCount}</span>
71141
</div>
72142
<ErrorBoundary
73143
// react-error-boundary's fallbackRender is a render-prop API by design;
74-
// the closure captures buildRetry which depends on parent state.
144+
// the closure captures retry state from this component.
75145
// eslint-disable-next-line react/no-unstable-nested-components
76146
fallbackRender={({ error, resetErrorBoundary }) => (
77-
<div className="bg-rose-50 border border-rose-200 rounded-lg p-4">
78-
<p className="text-rose-700 font-semibold mb-1">Server component fetch failed</p>
79-
<p className="text-rose-600 text-sm font-mono mb-3">{error.message}</p>
80-
<button
81-
type="button"
82-
onClick={buildRetry(resetErrorBoundary)}
83-
className="px-3 py-1.5 text-sm bg-rose-600 text-white rounded hover:bg-rose-700"
84-
>
85-
Retry
86-
</button>
87-
</div>
147+
<ActivityErrorFallback error={error} onRetry={buildBoundaryRetry(resetErrorBoundary)} />
88148
)}
89-
resetKeys={[refreshKey]}
149+
resetKeys={[routeRefreshKey]}
90150
>
91-
<Suspense fallback={<ActivityCardSkeleton />}>
92-
<RSCRoute componentName="LiveActivity" componentProps={{ simulateError, refreshKey }} />
93-
</Suspense>
151+
{fetchError ? (
152+
<ThrowActivityError error={fetchError} />
153+
) : (
154+
<Suspense fallback={<ActivityCardSkeleton />}>
155+
<RSCRoute componentName="LiveActivity" componentProps={componentProps} />
156+
</Suspense>
157+
)}
94158
</ErrorBoundary>
95159
</div>
96160
);

client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,15 @@ function ServerComponentsPage({ comments = [] }) {
6363
<h2 className="text-xl font-semibold text-slate-700 mb-4 flex items-center gap-2">
6464
Live Server Activity
6565
<span className="text-xs font-normal bg-indigo-100 text-indigo-700 px-2 py-0.5 rounded-full">
66-
RSCRoute + ErrorBoundary
66+
RSCRoute + Error Recovery
6767
</span>
6868
</h2>
6969
<p className="text-slate-500 text-sm mb-4">
7070
Click <strong>Refresh</strong> to fetch a new RSC payload — the server re-renders this section and
7171
streams the result back, no client-side JSON parsing or loading state plumbing. Click{' '}
7272
<strong>Simulate Error</strong> to make the server component throw; the failure surfaces as{' '}
73-
<code>ServerComponentFetchError</code> and is caught by <code>&lt;ErrorBoundary&gt;</code>, which
74-
renders a Retry button that calls <code>refetchComponent</code> with corrected props.
73+
<code>ServerComponentFetchError</code> through the RSC refetch API, and the Retry button calls{' '}
74+
<code>refetchComponent</code> with corrected props.
7575
</p>
7676
<LiveActivityRefresher />
7777
</section>

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@
8282
"react-dom": "~19.2.7",
8383
"react-error-boundary": "^4.1.2",
8484
"react-intl": "^6.4.4",
85-
"react-on-rails-pro": "17.0.0-rc.6",
86-
"react-on-rails-pro-node-renderer": "17.0.0-rc.6",
87-
"react-on-rails-rsc": "19.2.0",
85+
"react-on-rails-pro": "17.0.0-rc.7",
86+
"react-on-rails-pro-node-renderer": "17.0.0-rc.7",
87+
"react-on-rails-rsc": "19.2.1-rc.0",
8888
"react-redux": "^8.1.0",
8989
"react-router": "^6.13.0",
9090
"react-router-dom": "^6.13.0",

spec/system/server_components_demo_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
expect(page).to have_content("Refresh count: 1")
3333
end
3434

35-
it "shows the ErrorBoundary fallback when Simulate Error is clicked, then recovers on Retry" do
35+
it "shows the error recovery fallback when Simulate Error is clicked, then recovers on Retry" do
3636
click_button "Simulate Error"
3737
expect(page).to have_content("Server component fetch failed")
3838

yarn.lock

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8767,10 +8767,10 @@ react-is@^18.0.0, react-is@^18.3.1:
87678767
resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz"
87688768
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
87698769

8770-
react-on-rails-pro-node-renderer@17.0.0-rc.6:
8771-
version "17.0.0-rc.6"
8772-
resolved "https://registry.npmjs.org/react-on-rails-pro-node-renderer/-/react-on-rails-pro-node-renderer-17.0.0-rc.6.tgz#1fef0e21a7a13eb9d4525e05b6989a0ed79fb9c2"
8773-
integrity sha512-nX9Nmgb3hiUMPItC5xfV/W/FGnYLLzefr5qJOURPxrkHgEa6dw1ndnH2mRPyh0zFkOaCB2FBIy8lkWfXvRpfiA==
8770+
react-on-rails-pro-node-renderer@17.0.0-rc.7:
8771+
version "17.0.0-rc.7"
8772+
resolved "https://registry.npmjs.org/react-on-rails-pro-node-renderer/-/react-on-rails-pro-node-renderer-17.0.0-rc.7.tgz#dc5da170b71ae91cb6e53305f90645ee5c2d95c9"
8773+
integrity sha512-qyB0wd1AU7KsPMJJyc3U3l4rgq8ZVR8FOAqNQ4riZ2adqeNfz5k5Gx1NKwKMWd51ri7fGqyZDFC8295bAIQEZw==
87748774
dependencies:
87758775
"@fastify/formbody" "^7.4.0 || ^8.0.2"
87768776
"@fastify/multipart" "^8.3.1 || ^9.0.3"
@@ -8780,27 +8780,27 @@ react-on-rails-pro-node-renderer@17.0.0-rc.6:
87808780
lockfile "^1.0.4"
87818781
pino "^9.14.0 || ^10.1.0"
87828782

8783-
react-on-rails-pro@17.0.0-rc.6:
8784-
version "17.0.0-rc.6"
8785-
resolved "https://registry.npmjs.org/react-on-rails-pro/-/react-on-rails-pro-17.0.0-rc.6.tgz#41dc95ed0e3513822a2f0c9b4b25b34c607106be"
8786-
integrity sha512-Uc4IUmNYAJIk4iy4RatLN0qVM7MAi8zgXVvd7vZMEnVkGGhHqvj1A82ft97yJqgfqF9mm3eSPPyU3gyXrH5CsQ==
8783+
react-on-rails-pro@17.0.0-rc.7:
8784+
version "17.0.0-rc.7"
8785+
resolved "https://registry.npmjs.org/react-on-rails-pro/-/react-on-rails-pro-17.0.0-rc.7.tgz#d7f79b9c19e66061452bf2eecc64532da385339a"
8786+
integrity sha512-tfHNFjlX0Vnb/t4GxUc3X3mw742hHcrR3VapYfz3kAlDPNnpMi03PzPVPxwAO5FeQloHvAKq9/rjUemdsRDSQQ==
87878787
dependencies:
8788-
react-on-rails "17.0.0-rc.6"
8788+
react-on-rails "17.0.0-rc.7"
87898789

8790-
react-on-rails-rsc@19.2.0:
8791-
version "19.2.0"
8792-
resolved "https://registry.npmjs.org/react-on-rails-rsc/-/react-on-rails-rsc-19.2.0.tgz#3b539142048047193d524265fabdc81356bd482f"
8793-
integrity sha512-4PlfNFWGHl029al/yi10G3fcREbTwzqQ45uqYs6i9Baqds2OROyWp1xdorfX4Ok46IVJjje14SeaJVyN8pGDGg==
8790+
react-on-rails-rsc@19.2.1-rc.0:
8791+
version "19.2.1-rc.0"
8792+
resolved "https://registry.npmjs.org/react-on-rails-rsc/-/react-on-rails-rsc-19.2.1-rc.0.tgz#cf50da6586b8c978d3ff0699178e892e8cbde006"
8793+
integrity sha512-lWUQloOMYS2/Jio7M9pKaY84+gNJ1ZD2pMTj37HDZtf6GsJjb0CJfJlizvxbquUdMQawX+1h59c63syFZ7AWmQ==
87948794
dependencies:
87958795
acorn-loose "^8.3.0"
87968796
neo-async "^2.6.1"
87978797
react-server-dom-webpack "~19.2.7"
87988798
webpack-sources "^3.2.0"
87998799

8800-
react-on-rails@17.0.0-rc.6:
8801-
version "17.0.0-rc.6"
8802-
resolved "https://registry.npmjs.org/react-on-rails/-/react-on-rails-17.0.0-rc.6.tgz#4372994d13e7dcff4acaf06efad7b413da4bc42d"
8803-
integrity sha512-/u2hzYvuUN+1HnH0gEdbfQvkWkgLMSN2kl/2x9CCOInIHffBvilgXzBgJrP5NlvjWpguUWSxO6PrYpgV0BzLMA==
8800+
react-on-rails@17.0.0-rc.7:
8801+
version "17.0.0-rc.7"
8802+
resolved "https://registry.npmjs.org/react-on-rails/-/react-on-rails-17.0.0-rc.7.tgz#02c73fc20e68a7471aae391c9e5c7ed5ca3adaff"
8803+
integrity sha512-JWeDJeFUc8TEWtz4MWLMHpCIPBw3stKBx//EY8aPDZDwL1UxX3tnDDt/4JlIOzvGonz4ncGxEbEKcnBheyafDw==
88048804

88058805
react-proxy@^1.1.7:
88068806
version "1.1.8"

0 commit comments

Comments
 (0)