Skip to content

Commit df039e2

Browse files
ihabadhamclaude
andcommitted
Add RSCRoute + ErrorBoundary demo to RSC page
Demonstrates two RSC capabilities the demo previously didn't cover: 1. Client-fetched server components via `RSCRoute` — a new section "Live Server Activity" shows server-side data (timestamp, free RAM, uptime). A Refresh button bumps the props' refreshKey, causing `RSCRoute` to fetch a fresh RSC payload over HTTP. No client-side fetcher code, no JSON parsing, no loading-state plumbing — the server re-renders, streams the result back, and React reconciles. 2. Error handling per the docs' canonical pattern (`rsc-troubleshooting.md` "Error Boundary Limitations"): a Simulate Error button forces the server component to throw. The error surfaces on the client as `ServerComponentFetchError`, is caught by `<ErrorBoundary>` from `react-error-boundary`, and the fallback UI exposes a Retry button that re-fetches with corrected props. `LiveActivity.jsx` lives in `ror_components/` so auto_load_bundle registers it as a Server Component reachable by `RSCRoute` via name. `LiveActivityRefresher.jsx` is a single-file Client Component (no `.client/.server` wrapper pair needed) — registerServerComponent on the outer `ServerComponentsPage` provides the RSC context for `RSCRoute` calls anywhere in its subtree. Also restores `updated_at` to the comments prop set so the controller matches the canonical `_comment.json.jbuilder` partial's field shape (reverting a UI-driven minimization that coupled the resource representation to today's component code). Adds: `react-error-boundary@^4.1.2`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9e17d03 commit df039e2

6 files changed

Lines changed: 150 additions & 3 deletions

File tree

app/controllers/pages_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def rescript; end
4141

4242
def server_components
4343
@server_components_comments = Comment.order(id: :desc).limit(10)
44-
.as_json(only: %i[id author text created_at])
44+
.as_json(only: %i[id author text created_at updated_at])
4545
stream_view_containing_react_components(template: "/pages/server_components")
4646
end
4747

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use client';
2+
3+
import React, { useState } from 'react';
4+
import { ErrorBoundary } from 'react-error-boundary';
5+
import RSCRoute from 'react-on-rails-pro/RSCRoute';
6+
7+
function ErrorFallback({ error, resetErrorBoundary }) {
8+
return (
9+
<div className="bg-rose-50 border border-rose-200 rounded-lg p-4">
10+
<p className="text-rose-700 font-semibold mb-1">Server component fetch failed</p>
11+
<p className="text-rose-600 text-sm font-mono mb-3">{error.message}</p>
12+
<button
13+
type="button"
14+
onClick={resetErrorBoundary}
15+
className="px-3 py-1.5 text-sm bg-rose-600 text-white rounded hover:bg-rose-700"
16+
>
17+
Retry
18+
</button>
19+
</div>
20+
);
21+
}
22+
23+
const LiveActivityRefresher = () => {
24+
const [refreshKey, setRefreshKey] = useState(0);
25+
const [simulateError, setSimulateError] = useState(false);
26+
27+
const handleRefresh = () => {
28+
setSimulateError(false);
29+
setRefreshKey((k) => k + 1);
30+
};
31+
32+
const handleSimulateError = () => {
33+
setSimulateError(true);
34+
setRefreshKey((k) => k + 1);
35+
};
36+
37+
return (
38+
<div className="space-y-3">
39+
<div className="flex items-center gap-2">
40+
<button
41+
type="button"
42+
onClick={handleRefresh}
43+
className="px-3 py-1.5 text-sm bg-indigo-600 text-white rounded hover:bg-indigo-700"
44+
>
45+
Refresh
46+
</button>
47+
<button
48+
type="button"
49+
onClick={handleSimulateError}
50+
className="px-3 py-1.5 text-sm bg-amber-100 text-amber-800 border border-amber-300 rounded hover:bg-amber-200"
51+
>
52+
Simulate Error
53+
</button>
54+
<span className="text-xs text-slate-500 ml-2">
55+
Refresh count: {refreshKey}
56+
</span>
57+
</div>
58+
<ErrorBoundary
59+
FallbackComponent={ErrorFallback}
60+
onReset={() => {
61+
setSimulateError(false);
62+
setRefreshKey((k) => k + 1);
63+
}}
64+
resetKeys={[refreshKey]}
65+
>
66+
<RSCRoute componentName="LiveActivity" componentProps={{ simulateError, refreshKey }} />
67+
</ErrorBoundary>
68+
</div>
69+
);
70+
};
71+
72+
export default LiveActivityRefresher;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React from 'react';
2+
import os from 'os';
3+
4+
async function LiveActivity({ simulateError = false }) {
5+
if (simulateError) {
6+
throw new Error('Simulated server-side render failure (demo)');
7+
}
8+
9+
// Small delay so the refresh-in-flight state is visible.
10+
await new Promise((resolve) => {
11+
setTimeout(resolve, 300);
12+
});
13+
14+
const stats = {
15+
serverTime: new Date().toISOString(),
16+
freeMemoryMB: Math.round(os.freemem() / (1024 * 1024)),
17+
uptimeHours: Math.floor(os.uptime() / 3600),
18+
};
19+
20+
return (
21+
<div className="bg-gradient-to-br from-indigo-50 to-purple-50 border border-indigo-200 rounded-xl p-5">
22+
<div className="grid grid-cols-3 gap-4 text-sm">
23+
<div>
24+
<div className="text-xs text-indigo-600 font-medium uppercase tracking-wide mb-1">
25+
Server Time
26+
</div>
27+
<div className="font-mono text-indigo-900">{stats.serverTime}</div>
28+
</div>
29+
<div>
30+
<div className="text-xs text-indigo-600 font-medium uppercase tracking-wide mb-1">
31+
Free RAM
32+
</div>
33+
<div className="font-mono text-indigo-900">{stats.freeMemoryMB} MB</div>
34+
</div>
35+
<div>
36+
<div className="text-xs text-indigo-600 font-medium uppercase tracking-wide mb-1">
37+
Uptime (hrs)
38+
</div>
39+
<div className="font-mono text-indigo-900">{stats.uptimeHours}</div>
40+
</div>
41+
</div>
42+
</div>
43+
);
44+
}
45+
46+
export default LiveActivity;

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import React, { Suspense } from 'react';
66
import ServerInfo from '../components/ServerInfo';
77
import CommentsFeed from '../components/CommentsFeed';
88
import TogglePanel from '../components/TogglePanel';
9+
import LiveActivityRefresher from '../components/LiveActivityRefresher';
910

1011
const ServerComponentsPage = ({ comments = [] }) => {
1112
return (
@@ -57,6 +58,25 @@ const ServerComponentsPage = ({ comments = [] }) => {
5758
</TogglePanel>
5859
</section>
5960

61+
{/* Client-fetched server component via RSCRoute + ErrorBoundary */}
62+
<section>
63+
<h2 className="text-xl font-semibold text-slate-700 mb-4 flex items-center gap-2">
64+
Live Server Activity
65+
<span className="text-xs font-normal bg-indigo-100 text-indigo-700 px-2 py-0.5 rounded-full">
66+
RSCRoute + ErrorBoundary
67+
</span>
68+
</h2>
69+
<p className="text-slate-500 text-sm mb-4">
70+
Click <strong>Refresh</strong> to fetch a new RSC payload — the server re-renders
71+
this section and streams the result back, no client-side JSON parsing or loading
72+
state plumbing. Click <strong>Simulate Error</strong> to make the server component
73+
throw; the failure surfaces as <code>ServerComponentFetchError</code> and is
74+
caught by <code>&lt;ErrorBoundary&gt;</code>, which renders a Retry button that
75+
re-fetches with corrected props.
76+
</p>
77+
<LiveActivityRefresher />
78+
</section>
79+
6080
{/* Async data fetching with Suspense streaming */}
6181
<section>
6282
<h2 className="text-xl font-semibold text-slate-700 mb-4 flex items-center gap-2">
@@ -66,8 +86,9 @@ const ServerComponentsPage = ({ comments = [] }) => {
6686
</span>
6787
</h2>
6888
<p className="text-slate-500 text-sm mb-4">
69-
Comments are fetched directly on the server using the Rails API.
70-
The page shell renders immediately while this section streams in progressively.
89+
Comments come from the Rails controller as props — the canonical React on Rails Pro
90+
pattern. The page shell renders immediately while this section streams in
91+
progressively as Suspense boundaries resolve.
7192
</p>
7293
<Suspense
7394
fallback={

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"prop-types": "^15.8.1",
8181
"react": "~19.0.4",
8282
"react-dom": "~19.0.4",
83+
"react-error-boundary": "^4.1.2",
8384
"react-intl": "^6.4.4",
8485
"react-on-rails-pro": "16.6.0",
8586
"react-on-rails-pro-node-renderer": "16.6.0",

yarn.lock

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8745,6 +8745,13 @@ react-dom@~19.0.4:
87458745
dependencies:
87468746
scheduler "^0.25.0"
87478747

8748+
react-error-boundary@^4.1.2:
8749+
version "4.1.2"
8750+
resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz#bc750ad962edb8b135d6ae922c046051eb58f289"
8751+
integrity sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==
8752+
dependencies:
8753+
"@babel/runtime" "^7.12.5"
8754+
87488755
react-intl@^6.4.4:
87498756
version "6.8.9"
87508757
resolved "https://registry.npmjs.org/react-intl/-/react-intl-6.8.9.tgz"

0 commit comments

Comments
 (0)