Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
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
2 changes: 2 additions & 0 deletions Procfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ 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
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
5 changes: 5 additions & 0 deletions app/views/pages/server_components.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<%= append_javascript_pack_tag('rsc-client-components') %>
<%= react_component("ServerComponentsPage",
prerender: false,
trace: true,
Comment thread
justin808 marked this conversation as resolved.
Outdated
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 @@
'use client';

Check failure on line 1 in client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx

View workflow job for this annotation

GitHub Actions / test (22.x, 3.4.6)

File has too many classes (2). Maximum allowed is 1

// eslint-disable-next-line max-classes-per-file
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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
@@ -1,8 +1,10 @@
'use client';
Comment thread
justin808 marked this conversation as resolved.
Comment thread
justin808 marked this conversation as resolved.
Comment thread
justin808 marked this conversation as resolved.
Comment thread
justin808 marked this conversation as resolved.
Comment thread
justin808 marked this conversation as resolved.
Comment thread
justin808 marked this conversation as resolved.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading 'use client' directive. This is a traditional SSR component that uses StaticRouter and is only included in the server bundle — the RSC bundle explicitly excludes it (see the comment in rsc-bundle.js). The 'use client' directive here is dead code that contradicts the filename and could mislead future maintainers into thinking this file participates in the RSC graph. Remove the directive, or add a comment explaining why it is intentionally present.


// Compare to ./RouterApp.client.jsx
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
4 changes: 3 additions & 1 deletion client/app/bundles/comments/startup/serverRegistration.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use client';
Comment thread
justin808 marked this conversation as resolved.
Outdated
Comment thread
justin808 marked this conversation as resolved.
Outdated

// 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