This repository is a Hacker News demo built with Rails 8, React 19, and React on Rails Pro React Server Components (RSC).
It recreates the core experience of the Vercel next-react-server-components demo in a Rails-first application:
- Story feeds for
top,new,best,ask,show, andjob - Item pages with streamed nested comments
- User profile pages
- Rails-managed HTTP caching and 404 handling
- React Server Components rendered through the React on Rails Pro Node renderer
Reference projects:
- Public deployment: not configured yet
- Local demo after
bin/dev: http://localhost:3000
- Ruby
3.4.3 - Node.js
24.8.0 pnpm- PostgreSQL
- Optional:
miseto match.tool-versions
If you use mise:
mise install
bin/setup --skip-server
bin/devIf you manage runtimes yourself:
bundle install
pnpm install
bin/rails db:prepare
bin/devThen open http://localhost:3000.
bin/dev # Rails + client bundle + server bundle + node renderer + RSC bundle
bin/dev static # Static asset watch mode
bin/dev prod # Development with production-style assets
pnpm exec tsc --noEmit
bin/rubocop
bin/rails test
bin/rails test:system/and/news/:pagerender story feeds/item/:idrenders a story detail page or direct comment page/user/:idrenders a Hacker News user profile/rsc_payload/:component_namestreams the React Server Component payload used by React on Rails Pro
This app keeps Rails in charge of routing, controllers, caching, and HTML entrypoints, while React on Rails Pro handles the RSC runtime and the Node renderer.
- A Rails route hits
StoriesController,ItemsController, orUsersController. - The controller prepares props, preflights missing Hacker News resources, and applies HTTP caching headers.
- The
.erbview callsstream_react_component(...). - React on Rails Pro serves the initial shell and opens an RSC stream through
/rsc_payload/:component_name. - The Node renderer executes
rsc-bundle.js, resolves async server components, and streams Suspense boundaries back to the browser. - Only the minimal client code needed for interactivity is hydrated on the page.
app/controllers/manages route params, 404s, and cache headersapp/views/*/show.html.erbandapp/views/stories/index.html.erbare the Rails entrypoints for streamed componentslib/hacker_news_client.rbis the Ruby-side preflight client used for missing-resource checks
app/javascript/src/hn/ror_components/contains top-level route components registered with React on Rails Proapp/javascript/src/hn/components/contains async server components, presentational components, and minimal interactive surfacesapp/javascript/src/hn/lib/contains Hacker News API access, types, and view-model mappersclient/node-renderer.jsconfigures the React on Rails Pro Node renderer
- The demo uses Webpack for asset bundling because the current RSC manifest flow in this app depends on it
- The Node renderer runs separately from Rails in development on port
3800 hnApi.tsuses Nodehttpandhttpsinstead of relying on globalfetch, which keeps it compatible with the Node renderer VM
app/
controllers/
stories_controller.rb
items_controller.rb
users_controller.rb
views/
stories/index.html.erb
items/show.html.erb
users/show.html.erb
javascript/src/hn/
ror_components/
HNStoriesPage.tsx
HNItemPage.tsx
HNUserPage.tsx
components/
Stories.tsx
ItemPage.tsx
UserPage.tsx
Comments.tsx
Comment.tsx
CommentToggle.tsx
Story.client.tsx
lib/
hnApi.ts
mappers.ts
server.ts
types.ts
client/
node-renderer.js
config/
initializers/react_on_rails_pro.rb
webpack/
test/
integration/http_caching_test.rb
system/hacker_news_app_test.rb
support/
fake_hacker_news_api.rb
node_renderer_test_server.rb
Each route is still a normal Rails action plus a normal Rails view:
<%= stream_react_component("HNStoriesPage", props: @hn_stories_props, prerender: true) %>That keeps routing and response behavior in Rails instead of moving it into a JavaScript router.
Stories.tsx is an async server component. It fetches story IDs, then streams story rows behind Suspense boundaries:
export default async function Stories({ page, storyType }: StoriesProps) {
const storyPage = await fetchStoryPage(storyType, page);
return storyPage.ids.map((id, offset) => (
<Suspense fallback={<StoryRowFallback rank={offset + 1} />} key={id}>
<StoryRow id={id} rank={offset + 1} />
</Suspense>
));
}The same pattern is used for nested comments in Comments.tsx.
app/javascript/src/hn/lib/mappers.ts converts raw Hacker News payloads into stable view models before rendering. That keeps rendering code simple and avoids leaking API edge cases into components.
This demo keeps most logic on the server:
Story.client.tsxis a tiny client component for the story row surfaceCommentToggle.tsxuses native<details>and<summary>so comment collapsing works without a hook-based client boundary- Data fetching, pagination, comment recursion, and user pages all stay server-rendered
Controllers still own HTTP semantics:
apply_public_cache(ttl: 5.minutes, etag: [ "item", @hn_item_props[:itemId] ])That means the app can use Rails cache headers, ETags, and 404 responses without giving up streamed RSC rendering.
| Concern | Next.js Reference | This App |
|---|---|---|
| Routing | App Router / file-system routes | Rails routes + Rails controllers |
| Entry HTML | React/Next page tree | Rails view calls stream_react_component |
| Data fetching | Server components inside Next.js runtime | Server components inside React on Rails Pro Node renderer |
| RSC transport | Built into Next.js | Explicit /rsc_payload/:component_name route |
| HTTP caching | Typically handled in Next.js route handlers or hosting layer | Standard Rails controller cache headers and ETags |
| 404 handling | Next.js route conventions | Ruby preflight checks plus Rails status codes |
| Deployment shape | Next.js app runtime | Rails app plus Node renderer process |
The important similarity is the rendering model: async server components, Suspense boundaries, progressive streaming, and minimal client-side JavaScript.
The important difference is ownership: Rails remains the application shell, request orchestrator, and caching layer.
The test suite uses a deterministic fake Hacker News API plus a dedicated Node renderer test server.
test/integration/http_caching_test.rbverifies cache headers and 404 behaviortest/system/hacker_news_app_test.rbverifies feed rendering, nested comments, comment collapsing, and user pagestest/support/fake_hacker_news_api.rbremoves the external Hacker News API dependency during teststest/support/node_renderer_test_server.rbboots the renderer against compiled test assets
- React on Rails Pro docs
- Installation
- Configuration
- Node Renderer basics
- Add streaming and interactivity to an RSC page
- SSR React Server Components
Contributions are welcome.
- Open an issue before large scope changes
- Keep Rails routing/controller concerns separate from React rendering concerns
- Prefer server components by default and add client code only when interactivity requires it
- Run local checks before opening a pull request:
pnpm exec tsc --noEmit
bin/rubocop
bin/rails test
bin/rails test:system