Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ client/app/bundles/comments/rescript/**/*.bs.js
# Using React on Rails default directory
/ssr-generated/

# Node renderer bundle cache
.node-renderer-bundles/

# Generated React on Rails packs
**/generated/**

Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "3.4.6"

gem "react_on_rails", "16.6.0.rc.0"
gem "react_on_rails_pro", "16.5.1"
gem "shakapacker", "10.0.0.rc.0"

# Bundle edge Rails instead: gem "rails", github: "rails/rails"
Expand Down
36 changes: 34 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ GEM
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.3)
async (2.38.1)
console (~> 1.29)
fiber-annotation
io-event (~> 1.11)
metrics (~> 0.12)
traces (~> 0.18)
autoprefixer-rails (10.4.16.0)
execjs (~> 2)
awesome_print (1.9.2)
Expand Down Expand Up @@ -115,6 +121,10 @@ GEM
coffee-script-source (1.12.2)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
console (1.34.3)
fiber-annotation
fiber-local (~> 1.1)
json
coveralls_reborn (0.25.0)
simplecov (>= 0.18.1, < 0.22.0)
term-ansicolor (~> 1.6)
Expand Down Expand Up @@ -146,16 +156,24 @@ GEM
railties (>= 5.0.0)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
fiber-annotation (0.2.0)
fiber-local (1.1.0)
fiber-storage
fiber-storage (1.0.1)
foreman (0.88.1)
generator_spec (0.10.0)
activesupport (>= 3.0.0)
railties (>= 3.0.0)
globalid (1.3.0)
activesupport (>= 6.1)
http-2 (1.1.3)
httpx (1.7.5)
http-2 (>= 1.1.3)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
interception (0.5)
io-console (0.8.2)
io-event (1.14.5)
irb (1.17.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
Expand All @@ -165,6 +183,8 @@ GEM
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
json (2.19.1)
jwt (2.10.2)
base64
language_server-protocol (3.17.0.5)
launchy (3.0.1)
addressable (~> 2.8)
Expand All @@ -182,6 +202,7 @@ GEM
marcel (1.1.0)
matrix (0.4.2)
method_source (1.1.0)
metrics (0.15.0)
mini_mime (1.1.5)
minitest (6.0.2)
drb (~> 2.0)
Expand Down Expand Up @@ -296,13 +317,23 @@ GEM
erb
psych (>= 4.0.0)
tsort
react_on_rails (16.6.0.rc.0)
react_on_rails (16.5.1)
addressable
connection_pool
execjs (~> 2.5)
rails (>= 5.2)
rainbow (~> 3.0)
shakapacker (>= 6.0)
react_on_rails_pro (16.5.1)
addressable
async (>= 2.29)
connection_pool
execjs (~> 2.9)
http-2 (>= 1.1.1)
httpx (~> 1.5)
jwt (~> 2.7)
rainbow
react_on_rails (= 16.5.1)
redcarpet (3.6.0)
redis (5.3.0)
redis-client (>= 0.22.0)
Expand Down Expand Up @@ -425,6 +456,7 @@ GEM
tins (1.33.0)
bigdecimal
sync
traces (0.18.2)
tsort (0.2.0)
turbo-rails (2.0.11)
actionpack (>= 6.0.0)
Expand Down Expand Up @@ -486,7 +518,7 @@ DEPENDENCIES
rails-html-sanitizer
rails_best_practices
rainbow
react_on_rails (= 16.6.0.rc.0)
react_on_rails_pro (= 16.5.1)
redcarpet
redis (~> 5.0)
rspec-rails (~> 6.0.0)
Expand Down
4 changes: 4 additions & 0 deletions Procfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ rails: bundle exec thrust bin/rails server -p 3000
wp-client: RAILS_ENV=development NODE_ENV=development bin/shakapacker-dev-server
# Server Rspack watcher for SSR bundle
wp-server: SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch
# RSC Rspack watcher for React Server Components bundle
wp-rsc: RSC_BUNDLE_ONLY=true bin/shakapacker --watch
# React on Rails Pro Node renderer for SSR and RSC payload generation
node-renderer: RENDERER_PASSWORD=devPassword node react-on-rails-pro-node-renderer.js
Comment thread
justin808 marked this conversation as resolved.
Outdated
2 changes: 2 additions & 0 deletions app/controllers/pages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def simple; end

def rescript; end

def server_components; end

private

def set_comments
Expand Down
6 changes: 6 additions & 0 deletions app/views/pages/server_components.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<%= append_javascript_pack_tag('rsc-client-components') %>
<%= react_component("ServerComponentsPage",
prerender: false,
auto_load_bundle: false,
trace: Rails.env.development?,
id: "ServerComponentsPage-react-component-0") %>
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import React from 'react';
import PropTypes from 'prop-types';
import BaseComponent from 'libs/components/BaseComponent';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ function NavigationBar(props) {
Rescript
</a>
</li>
<li>
<a
className={navItemClassName(pathname === paths.SERVER_COMPONENTS_PATH)}
href={paths.SERVER_COMPONENTS_PATH}
>
RSC Demo
</a>
</li>
<li>
<a
className={navItemClassName(false)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// eslint-disable-next-line max-classes-per-file
/* eslint-disable max-classes-per-file */
'use client';

import React from 'react';
import request from 'axios';
import Immutable from 'immutable';
import _ from 'lodash';
import ReactOnRails from 'react-on-rails';
import ReactOnRails from 'react-on-rails-pro';
import { IntlProvider, injectIntl } from 'react-intl';
import BaseComponent from 'libs/components/BaseComponent';
import SelectLanguage from 'libs/i18n/selectLanguage';
Expand Down
1 change: 1 addition & 0 deletions client/app/bundles/comments/constants/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export const RESCRIPT_PATH = '/rescript';
export const SIMPLE_REACT_PATH = '/simple';
export const STIMULUS_PATH = '/stimulus';
export const RAILS_PATH = '/comments';
export const SERVER_COMPONENTS_PATH = '/server-components';
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

// Wrapper for ReScript component to work with react_on_rails auto-registration
// react_on_rails looks for components in ror_components/ subdirectories

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use client';

import { Provider } from 'react-redux';
import React from 'react';
import ReactOnRails from 'react-on-rails';
import ReactOnRails from 'react-on-rails-pro';

import NonRouterCommentsContainer from '../../../containers/NonRouterCommentsContainer.jsx';
import 'intl/locale-data/jsonp/en';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Compare to ../ServerRouterApp.jsx
import { Provider } from 'react-redux';
import React from 'react';
import ReactOnRails from 'react-on-rails';
import ReactOnRails from 'react-on-rails-pro';
import { Router, browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'use client';

// Top level component for client side.
// Compare this to the ./ServerApp.jsx file which is used for server side rendering.

import { Provider } from 'react-redux';
import React from 'react';
import ReactOnRails from 'react-on-rails';
import ReactOnRails from 'react-on-rails-pro';

import NavigationBar from '../../../components/NavigationBar/NavigationBar.jsx';
import NavigationBarContainer from '../../../containers/NavigationBarContainer.jsx';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
'use client';

// Compare to ./RouterApp.server.jsx
import { Provider } from 'react-redux';
import React from 'react';
import ReactOnRails from 'react-on-rails';
import ReactOnRails from 'react-on-rails-pro';
import { BrowserRouter } from 'react-router-dom';
import routes from '../../../routes/routes.jsx';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { Provider } from 'react-redux';
import React from 'react';
import { StaticRouter } from 'react-router-dom/server';
import ReactOnRails from 'react-on-rails';
import ReactOnRails from 'react-on-rails-pro';
import routes from '../../../routes/routes.jsx';

function ServerRouterApp(_props, railsContext) {
Expand Down
2 changes: 1 addition & 1 deletion client/app/bundles/comments/startup/serverRegistration.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Example of React + Redux
import ReactOnRails from 'react-on-rails';
import ReactOnRails from 'react-on-rails-pro';

import App from './App/ror_components/App';
import RouterApp from './RouterApp/ror_components/RouterApp.server';
Expand Down
129 changes: 129 additions & 0 deletions client/app/bundles/server-components/ServerComponentsPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Server Component - this entire component runs on the server.
// It can use Node.js APIs and server-only dependencies directly.
// None of these imports are shipped to the client bundle.

import React, { Suspense } from 'react';
import ServerInfo from './components/ServerInfo';
import CommentsFeed from './components/CommentsFeed';
import TogglePanel from './components/TogglePanel';

const ServerComponentsPage = () => {
return (
<div className="max-w-4xl mx-auto py-8 px-4">
<header className="mb-10">
<h1 className="text-3xl font-bold text-slate-800 mb-2">
React Server Components Demo
</h1>
<p className="text-slate-600 text-lg">
This page is rendered using <strong>React Server Components</strong> with React on Rails Pro.
Server components run on the server and stream their output to the client, keeping
heavy dependencies out of the browser bundle entirely.
</p>
</header>

<div className="space-y-8">
{/* Server Info - uses Node.js os module (impossible on client) */}
<section>
<h2 className="text-xl font-semibold text-slate-700 mb-4 flex items-center gap-2">
Server Environment
<span className="text-xs font-normal bg-emerald-100 text-emerald-700 px-2 py-0.5 rounded-full">
Server Only
</span>
</h2>
<ServerInfo />
</section>

{/* Interactive toggle - demonstrates mixing server + client components */}
<section>
<h2 className="text-xl font-semibold text-slate-700 mb-4 flex items-center gap-2">
Interactive Client Component
<span className="text-xs font-normal bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full">
Client Hydrated
</span>
</h2>
<TogglePanel title="How does this work?">
<div className="prose prose-slate max-w-none text-sm">
<p>
This toggle is a <code>&apos;use client&apos;</code> component, meaning it ships JavaScript
to the browser for interactivity. But the content inside is rendered on the server
and passed as children — a key RSC pattern called the <strong>donut pattern</strong>.
</p>
<ul>
<li>The <code>TogglePanel</code> wrapper runs on the client (handles click events)</li>
<li>The children content is rendered on the server (no JS cost)</li>
<li>Heavy libraries used by server components never reach the browser</li>
</ul>
</div>
</TogglePanel>
</section>

{/* Async data fetching with Suspense streaming */}
<section>
<h2 className="text-xl font-semibold text-slate-700 mb-4 flex items-center gap-2">
Streamed Comments
<span className="text-xs font-normal bg-amber-100 text-amber-700 px-2 py-0.5 rounded-full">
Async + Suspense
</span>
</h2>
<p className="text-slate-500 text-sm mb-4">
Comments are fetched directly on the server using the Rails API.
The page shell renders immediately while this section streams in progressively.
Comment thread
justin808 marked this conversation as resolved.
</p>
<Suspense
Comment thread
justin808 marked this conversation as resolved.
fallback={
<div className="animate-pulse space-y-3">
{[1, 2, 3].map((i) => (
<div key={i} className="bg-slate-100 rounded-lg p-4">
<div className="h-4 bg-slate-200 rounded w-1/4 mb-2" />
<div className="h-3 bg-slate-200 rounded w-3/4" />
</div>
))}
</div>
}
>
<CommentsFeed />
</Suspense>
</section>

{/* Architecture explanation */}
<section className="bg-slate-50 border border-slate-200 rounded-xl p-6">
<h2 className="text-lg font-semibold text-slate-700 mb-3">
What makes this different?
</h2>
<div className="grid md:grid-cols-2 gap-4 text-sm text-slate-600">
<div>
<h3 className="font-medium text-slate-800 mb-1">Smaller Client Bundle</h3>
<p>
Libraries like <code>lodash</code>, <code>marked</code>, and Node.js <code>os</code> module
are used on this page but never downloaded by the browser.
</p>
</div>
<div>
<h3 className="font-medium text-slate-800 mb-1">Direct Data Access</h3>
<p>
Server components fetch data by calling your Rails API internally — no
client-side fetch waterfalls or loading spinners for initial data.
</p>
</div>
<div>
<h3 className="font-medium text-slate-800 mb-1">Progressive Streaming</h3>
<p>
The page shell renders instantly. Async components (like the comments feed)
stream in as their data resolves, with Suspense boundaries showing fallbacks.
</p>
</div>
<div>
<h3 className="font-medium text-slate-800 mb-1">Selective Hydration</h3>
<p>
Only client components (like the toggle above) receive JavaScript.
Everything else is pure HTML — zero hydration cost.
</p>
</div>
</div>
</section>
</div>
</div>
);
};

export default ServerComponentsPage;
Loading
Loading