diff --git a/.github/workflows/rspec_test.yml b/.github/workflows/rspec_test.yml index d117458a6..5ebcb89da 100644 --- a/.github/workflows/rspec_test.yml +++ b/.github/workflows/rspec_test.yml @@ -33,6 +33,7 @@ jobs: DRIVER: selenium_chrome CHROME_BIN: /usr/bin/google-chrome USE_COVERALLS: true + RENDERER_PASSWORD: devPassword steps: - name: Install Chrome @@ -82,6 +83,20 @@ jobs: - name: Build shakapacker chunks run: NODE_ENV=development bundle exec bin/shakapacker + - name: Start Node renderer for SSR + run: | + node react-on-rails-pro-node-renderer.js & + echo "Waiting for Node renderer on port 3800..." + for i in $(seq 1 30); do + if nc -z localhost 3800 2>/dev/null; then + echo "Node renderer is ready" + exit 0 + fi + sleep 1 + done + echo "Node renderer failed to start within 30 seconds" + exit 1 + - name: Run rspec with xvfb uses: coactions/setup-xvfb@v1 with: diff --git a/.gitignore b/.gitignore index 366e6a9b7..5c7b739bb 100644 --- a/.gitignore +++ b/.gitignore @@ -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/** diff --git a/Gemfile b/Gemfile index e9eb85053..7acd9bfc0 100644 --- a/Gemfile +++ b/Gemfile @@ -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" diff --git a/Gemfile.lock b/Gemfile.lock index b290c3986..3e0ee2285 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) diff --git a/Procfile.dev b/Procfile.dev index 102c0a8df..d37228687 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -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: node react-on-rails-pro-node-renderer.js diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 507cc6cf7..f435f04eb 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -38,6 +38,8 @@ def simple; end def rescript; end + def server_components; end + private def set_comments diff --git a/app/views/pages/server_components.html.erb b/app/views/pages/server_components.html.erb new file mode 100644 index 000000000..d9643a213 --- /dev/null +++ b/app/views/pages/server_components.html.erb @@ -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") %> diff --git a/client/app/bundles/comments/components/Footer/ror_components/Footer.jsx b/client/app/bundles/comments/components/Footer/ror_components/Footer.jsx index 5e7f42104..d153dfb22 100644 --- a/client/app/bundles/comments/components/Footer/ror_components/Footer.jsx +++ b/client/app/bundles/comments/components/Footer/ror_components/Footer.jsx @@ -1,3 +1,5 @@ +'use client'; + import React from 'react'; import PropTypes from 'prop-types'; import BaseComponent from 'libs/components/BaseComponent'; diff --git a/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx b/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx index db2b4e53c..30b99f371 100644 --- a/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx +++ b/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx @@ -102,6 +102,14 @@ function NavigationBar(props) { Rescript +
+ This page is rendered using React Server Components 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. +
+
+ This toggle is a 'use client' 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 donut pattern.
+
TogglePanel wrapper runs on the client (handles click events)+ Comments are fetched directly on the server using the Rails API. + The page shell renders immediately while this section streams in progressively. +
+
+ Libraries like lodash, marked, and Node.js os module
+ are used on this page but never downloaded by the browser.
+
+ Server components fetch data by calling your Rails API internally — no + client-side fetch waterfalls or loading spinners for initial data. +
++ The page shell renders instantly. Async components (like the comments feed) + stream in as their data resolves, with Suspense boundaries showing fallbacks. +
++ Only client components (like the toggle above) receive JavaScript. + Everything else is pure HTML — zero hydration cost. +
+Could not load comments right now. Please try again later.
++ No comments yet. Add some comments from the{' '} + + home page + {' '} + to see them rendered here by server components. +
+{comment.text}
+
+ {recentComments.length} comment{recentComments.length !== 1 ? 's' : ''} rendered on the server using{' '}
+ marked + sanitize-html (never sent to browser)
+
+ This data comes from the Node.js os module
+ — it runs only on the server. The lodash library
+ used to format it never reaches the browser.
+