Skip to content

Commit c3f432b

Browse files
Use RSC payload to render server components on server (#1696)
* add needed utils to use rsc payload on the server * Refactor component registration and rendering logic to support `server-component-reference` type * Revert "Refactor component registration and rendering logic to support `server-component-reference` type" This reverts commit 8df9e11. * Enhance ReactOnRails options management * Revert "Enhance ReactOnRails options management" This reverts commit 2f99ea9. * add support for returning promise of react component from render function * Update ReactOnRails configuration to rename server manifest file for consistency. Adjust related methods and references to use the new naming convention for the server client manifest file. * Refactor ReactOnRails to support React Server Components (RSC) registration and rendering * embed rsc payload inside the html page * linting * fix ts errors * tmp * linting * linting * Update RSCClientRoot and related files to use RSCPayloadChunk type for improved type safety in RSC streaming and handling. * Reset render state after processing each chunk to prevent error carryover in stream rendering. * Update RailsContext type to make rscPayloadGenerationUrl optional * Fix formatting in ReactOnRails helper for consistency * Update error handling in createPromiseResult to return errorRenderState instead of renderState * Refactor RSCServerRoot to use RailsContext in generateRSCPayload and improve error messaging for missing manifest files * Add .yalc directory to ESLint ignore patterns * update changelog * rename __FLIGHT_DATA to REACT_ON_RAILS_RSC_PAYLOAD for improved clarity and consistency in RSC streaming handling. * Enhance type safety in RSCClientRoot and RSCPayloadContainer by using RSCPayloadChunk type, and improve documentation for chunk handling and transfer resilience. * convert createElement calls to jsx * rename transformRSCNodeStreamAndReplayConsoleLogs to transformRSCNodeStream * Enhance RSCPayloadContainer documentation on escape sequences and modify chunk index validation in RSCServerRoot for improved error handling. * refactoring * remove unnecessary keys and streamline component rendering, enhancing code clarity and maintainability. * Update import paths in server.rsc.ts to include file extensions for improved clarity and consistency. * Update import statement in server.ts to use .tsx extension for RSCServerRoot * Add ability to render server components inside client components (add support for react-router) (#1736) * replace RSCPayloadContainer with RSC injection utils * Add renderRequestId to rendering options and context for improved request tracking - Introduced renderRequestId in render options to uniquely identify each render request. - Updated server rendering code to include renderRequestId in the context. - Enhanced client-side rendering to handle and log renderRequestId. - Modified type definitions to accommodate the new renderRequestId field in RailsContext. - Improved debugging output in RailsContext component for better visibility of context data. * tmp * trace used rsc payload streams on server and pass them to client * replace registerRouter with wrapServerComponentRenderer utility function * add artifical delay amd log statements * Revert "add artifical delay amd log statements" This reverts commit cd57337. * Update import statements to include file extensions * add missing docs and specs * remove RSCRoots and replace its test with registerServerComponent tests * update injectRSCPayload tests to expect the new behavior * fix streamServerRenderedReactComponent and helper specs * Update renderContextRows to exclude 'componentSpecificMetadata' from rendering * fix knip errors * Remove RSCServerRoot entry from package.json * add test to test the behavior of hydrating Suspensable components * initialize the rsc payload array in a sync manner when the generate rsc function is called to avoid hydration race conditions * fix failing jest tests * add TypeScript ignore comment in SuspenseHydration test for Node 18+ compatibility * remove options parameter from registerServerComponent and update related tests * refactor: rename WrapServerComponentRenderer to wrapServerComponentRenderer * docs: add detailed JSDoc comments for RSC functions and utilities to improve code clarity and maintainability * feat: implement post-SSR hooks for enhanced server-side rendering control and update package dependencies * refactor: improve type safety in server component loading and streamline import statements * fix: update module loading configuration to use environment-specific prefix and set crossOrigin to null * refactor: update react-on-rails-rsc dependency to use SSR support and streamline imports for server components * chore: update acorn and acorn-loose dependencies in yarn.lock to latest versions for improved compatibility * refactor: clarify test description for child async component hydration in SuspenseHydration test * fix problem of returning the wrong stream * refactor: update RailsContext types to include server component capabilities and streamline related functions * refactor: rename rscPayloadGenerationUrl to rscPayloadGenerationUrlPath and enhance type safety in RailsContext * refactor: enhance error handling and improve code clarity in server component rendering and tests * handle trailing commas while removing packages from at CI "oldest" tests * don't run SuspenseHydration tests with CI oldest tests * chore: update testPathIgnorePatterns to exclude additional test cases in CI * chore: escape quotes in testPathIgnorePatterns for proper parsing in CI * refactor: update RailsContext usage in tests to utilize RailsContextWithServerComponentCapabilities for improved type safety * refactor: update test cases to use rscPayloadGenerationUrlPath for consistency in RailsContext * refactor: unify stream types by introducing PipeableOrReadableStream * refactor: replace direct checks for RSC support with a utility method for improved clarity and maintainability * refactor: update stubbing in packs_generator_spec to improve clarity and maintainability * refactor: make renderRequestId optional in server component rendering for improved flexibility and clarity * refactor: remove renderRequestId from script tags in ReactOnRailsHelper specs for improved clarity * refactor: optimize RSC payload handling and improve hook management for better performance and clarity * refactor: ensure RSC support variable is reset in packs_generator_spec for improved test isolation * refactor: enhance RSC component handling by introducing promise wrapper and improving stream processing for better error management * refactor: make serverSideRSCPayloadParameters optional in RailsContextWithServerComponentCapabilities for improved flexibility * handle error happen during rsc payload generation * add rsc payload url to context only if rsc support enabled * removed unneeded rubocop disable statement * Enhance error handling and memory management in RSC payload processing * add a bility to refetch and retry fetching of server components * Update import statements in client and server components to use .tsx extension for RSCRoute * removed the functionality of caching components, react already caches them * delete unused file and update knip configs * Refactor RSC handling to improve performance and error management * Fix test expectations for fetch calls in registerServerComponent tests to use URL-encoded props format. * Enhance error handling in server component rendering and RSC payload processing by adding catch blocks for promise rejections and error events in streams. * Update react-on-rails-rsc dependency to version 19.0.1 in package.json and yarn.lock * update react to v19.1.0 * Revert "update react to v19.1.0" This reverts commit 9206e01. * Update react-on-rails-rsc dependency to version 19.0.2 in package.json and yarn.lock * ensure to send the rsc payload array initilization before the html chunk * Update changelog.md
1 parent 57938d0 commit c3f432b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+2433
-480
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ After a release, please make sure to run `bundle exec rake update_changelog`. Th
2323

2424
Changes since the last non-beta release.
2525

26+
#### Improved
27+
28+
- Improved RSC rendering flow by eliminating double rendering of server components and reducing the number of HTTP requests.
29+
- Updated communication protocol between Node Renderer and Rails to version 2.0.0 which supports the ability to upload multiple bundles at once.
30+
- Added `RSCRoute` component to enable seamless server-side rendering of React Server Components. This component automatically handles RSC payload injection and hydration, allowing server components to be rendered directly within client components while maintaining optimal performance.
31+
32+
[PR 1696](https://github.com/shakacode/react_on_rails/pull/1696) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
33+
2634
#### Added
2735

2836
- Configuration option `generated_component_packs_loading_strategy` to control how generated component packs are loaded. It supports `sync`, `async`, and `defer` strategies. [PR 1712](https://github.com/shakacode/react_on_rails/pull/1712) by [AbanoubGhadban](https://github.com/AbanoubGhadban).

docs/guides/configuration.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,31 @@ ReactOnRails.configure do |config|
104104
# you should include a name that matches your bundle name in your Webpack config.
105105
config.server_bundle_js_file = "server-bundle.js"
106106

107+
# When using React on Rails Pro with RSC support enabled, these configuration options work together:
108+
#
109+
# 1. In RORP, set `config.enable_rsc_support = true` in your react_on_rails_pro.rb initializer
110+
#
111+
# 2. The `rsc_bundle_js_file` (typically "rsc-bundle.js") contains only server components and
112+
# references to client components. It's generated using the RSC Webpack Loader which transforms
113+
# client components into references. This bundle is specifically used for generating RSC payloads
114+
# and is configured with the `react-server` condition.
115+
config.rsc_bundle_js_file = "rsc-bundle.js"
116+
#
117+
# 3. The `react_client_manifest_file` contains mappings for client components that need hydration.
118+
# It's generated by the React Server Components Webpack plugin and is required for client-side
119+
# hydration of components.
120+
# This manifest file is automatically generated by the React Server Components Webpack plugin. Only set this if you've configured the plugin to use a different filename.
121+
config.react_client_manifest_file = "react-client-manifest.json"
122+
#
123+
# 4. The `react_server_client_manifest_file` is used during server-side rendering with RSC to
124+
# properly resolve references between server and client components.
125+
#
126+
# These files are crucial when implementing React Server Components with streaming, which offers
127+
# benefits like reduced JavaScript bundle sizes, faster page loading, and selective hydration
128+
# of client components.
129+
# This manifest file is automatically generated by the React Server Components Webpack plugin. Only set this if you've configured the plugin to use a different filename.
130+
config.react_server_client_manifest_file = "react-server-client-manifest.json"
131+
107132
# `prerender` means server-side rendering
108133
# default is false. This is an option for view helpers `render_component` and `render_component_hash`.
109134
# Set to true to change the default value to true.

eslint.config.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const config = tsEslint.config([
4242
'**/node_modules/',
4343
// fixtures
4444
'**/fixtures/',
45+
'**/.yalc/**/*',
4546
]),
4647
{
4748
files: ['**/*.[jt]s', '**/*.[jt]sx', '**/*.[cm][jt]s'],
@@ -71,7 +72,7 @@ const config = tsEslint.config([
7172
alias: [['Assets', './spec/dummy/client/app/assets']],
7273

7374
node: {
74-
extensions: ['.js', '.jsx', '.ts', '.d.ts'],
75+
extensions: ['.js', '.jsx', '.ts', '.tsx', '.d.ts'],
7576
},
7677
},
7778
},
@@ -96,6 +97,7 @@ const config = tsEslint.config([
9697
js: 'never',
9798
jsx: 'never',
9899
ts: 'never',
100+
tsx: 'never',
99101
},
100102
],
101103

@@ -128,6 +130,12 @@ const config = tsEslint.config([
128130
'react/jsx-props-no-spreading': 'off',
129131
'react/static-property-placement': 'off',
130132
'jsx-a11y/anchor-is-valid': 'off',
133+
'react/jsx-filename-extension': [
134+
'error',
135+
{
136+
extensions: ['.jsx', '.tsx'],
137+
},
138+
],
131139
},
132140
},
133141
{
@@ -153,7 +161,7 @@ const config = tsEslint.config([
153161
languageOptions: {
154162
parserOptions: {
155163
projectService: {
156-
allowDefaultProject: ['eslint.config.ts', 'knip.ts', 'node_package/tests/*.test.ts'],
164+
allowDefaultProject: ['eslint.config.ts', 'knip.ts', 'node_package/tests/*.test.{ts,tsx}'],
157165
// Needed because `import * as ... from` instead of `import ... from` doesn't work in this file
158166
// for some imports.
159167
defaultProject: 'tsconfig.eslint.json',

jest.config.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ export default {
1414
}),
1515
testEnvironment: 'jsdom',
1616
setupFiles: ['<rootDir>/node_package/tests/jest.setup.js'],
17-
// React Server Components tests are compatible with React 19
18-
// That only run with node version 18 and above
17+
// React Server Components tests require React 19 and only run with Node version 18 (`newest` in our CI matrix)
1918
moduleNameMapper:
2019
nodeVersion < 18
2120
? {

knip.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ const config: KnipConfig = {
77
entry: [
88
'node_package/src/ReactOnRails.node.ts!',
99
'node_package/src/ReactOnRailsRSC.ts!',
10-
'node_package/src/registerServerComponent/client.ts!',
11-
'node_package/src/registerServerComponent/server.ts!',
12-
'node_package/src/RSCClientRoot.ts!',
10+
'node_package/src/registerServerComponent/client.tsx!',
11+
'node_package/src/registerServerComponent/server.tsx!',
12+
'node_package/src/registerServerComponent/server.rsc.ts!',
13+
'node_package/src/wrapServerComponentRenderer/server.tsx!',
14+
'node_package/src/wrapServerComponentRenderer/server.rsc.tsx!',
15+
'node_package/src/RSCRoute.tsx!',
16+
'node_package/src/ServerComponentFetchError.ts!',
1317
'eslint.config.ts',
1418
],
1519
project: ['node_package/src/**/*.[jt]s{x,}!', 'node_package/tests/**/*.[jt]s{x,}'],

lib/react_on_rails/configuration.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ def self.configure
1010

1111
DEFAULT_GENERATED_ASSETS_DIR = File.join(%w[public webpack], Rails.env).freeze
1212
DEFAULT_REACT_CLIENT_MANIFEST_FILE = "react-client-manifest.json"
13+
DEFAULT_REACT_SERVER_CLIENT_MANIFEST_FILE = "react-server-client-manifest.json"
1314
DEFAULT_COMPONENT_REGISTRY_TIMEOUT = 5000
1415

1516
def self.configuration
@@ -21,6 +22,7 @@ def self.configuration
2122
server_bundle_js_file: "",
2223
rsc_bundle_js_file: "",
2324
react_client_manifest_file: DEFAULT_REACT_CLIENT_MANIFEST_FILE,
25+
react_server_client_manifest_file: DEFAULT_REACT_SERVER_CLIENT_MANIFEST_FILE,
2426
prerender: false,
2527
auto_load_bundle: false,
2628
replay_console: true,
@@ -66,7 +68,7 @@ class Configuration
6668
:same_bundle_for_client_and_server, :rendering_props_extension,
6769
:make_generated_server_bundle_the_entrypoint,
6870
:generated_component_packs_loading_strategy, :force_load, :rsc_bundle_js_file,
69-
:react_client_manifest_file, :component_registry_timeout
71+
:react_client_manifest_file, :react_server_client_manifest_file, :component_registry_timeout
7072

7173
# rubocop:disable Metrics/AbcSize
7274
def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender: nil,
@@ -82,7 +84,8 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
8284
i18n_dir: nil, i18n_yml_dir: nil, i18n_output_format: nil, i18n_yml_safe_load_options: nil,
8385
random_dom_id: nil, server_render_method: nil, rendering_props_extension: nil,
8486
components_subdirectory: nil, auto_load_bundle: nil, force_load: nil,
85-
rsc_bundle_js_file: nil, react_client_manifest_file: nil, component_registry_timeout: nil)
87+
rsc_bundle_js_file: nil, react_client_manifest_file: nil, react_server_client_manifest_file: nil,
88+
component_registry_timeout: nil)
8689
self.node_modules_location = node_modules_location.present? ? node_modules_location : Rails.root
8790
self.generated_assets_dirs = generated_assets_dirs
8891
self.generated_assets_dir = generated_assets_dir
@@ -112,6 +115,7 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
112115
self.server_bundle_js_file = server_bundle_js_file
113116
self.rsc_bundle_js_file = rsc_bundle_js_file
114117
self.react_client_manifest_file = react_client_manifest_file
118+
self.react_server_client_manifest_file = react_server_client_manifest_file
115119
self.same_bundle_for_client_and_server = same_bundle_for_client_and_server
116120
self.server_renderer_pool_size = self.development_mode ? 1 : server_renderer_pool_size
117121
self.server_renderer_timeout = server_renderer_timeout # seconds
@@ -305,7 +309,8 @@ def ensure_webpack_generated_files_exists
305309
"manifest.json",
306310
server_bundle_js_file,
307311
rsc_bundle_js_file,
308-
react_client_manifest_file
312+
react_client_manifest_file,
313+
react_server_client_manifest_file
309314
].compact_blank
310315
end
311316

lib/react_on_rails/helper.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,14 @@ def rails_context(server_side: true)
373373
# TODO: v13 just use the version if existing
374374
rorPro: ReactOnRails::Utils.react_on_rails_pro?
375375
}
376+
376377
if ReactOnRails::Utils.react_on_rails_pro?
377378
result[:rorProVersion] = ReactOnRails::Utils.react_on_rails_pro_version
379+
380+
if ReactOnRails::Utils.rsc_support_enabled?
381+
rsc_payload_url = ReactOnRailsPro.configuration.rsc_payload_generation_url_path
382+
result[:rscPayloadGenerationUrlPath] = rsc_payload_url
383+
end
378384
end
379385

380386
if defined?(request) && request.present?
@@ -644,7 +650,8 @@ def internal_react_component(react_component_name, options = {})
644650
"data-trace" => (render_options.trace ? true : nil),
645651
"data-dom-id" => render_options.dom_id,
646652
"data-store-dependencies" => render_options.store_dependencies&.to_json,
647-
"data-force-load" => (render_options.force_load ? true : nil))
653+
"data-force-load" => (render_options.force_load ? true : nil),
654+
"data-render-request-id" => render_options.render_request_id)
648655

649656
if render_options.force_load
650657
component_specification_tag.concat(

lib/react_on_rails/packs_generator.rb

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,18 +89,13 @@ def client_entrypoint?(file_path)
8989

9090
def pack_file_contents(file_path)
9191
registered_component_name = component_name(file_path)
92-
load_server_components = ReactOnRails::Utils.react_on_rails_pro? &&
93-
ReactOnRailsPro.configuration.enable_rsc_support
92+
load_server_components = ReactOnRails::Utils.rsc_support_enabled?
9493

9594
if load_server_components && !client_entrypoint?(file_path)
96-
rsc_payload_generation_url_path = ReactOnRailsPro.configuration.rsc_payload_generation_url_path
97-
9895
return <<~FILE_CONTENT.strip
9996
import registerServerComponent from 'react-on-rails/registerServerComponent/client';
10097
101-
registerServerComponent({
102-
rscPayloadGenerationUrlPath: "#{rsc_payload_generation_url_path}",
103-
}, "#{registered_component_name}")
98+
registerServerComponent("#{registered_component_name}");
10499
FILE_CONTENT
105100
end
106101

@@ -146,8 +141,7 @@ def generated_server_pack_file_content
146141
"import #{name} from '#{relative_path(generated_server_bundle_file_path, component_path)}';"
147142
end
148143

149-
load_server_components = ReactOnRails::Utils.react_on_rails_pro? &&
150-
ReactOnRailsPro.configuration.enable_rsc_support
144+
load_server_components = ReactOnRails::Utils.rsc_support_enabled?
151145
server_components = component_for_server_registration_to_path.keys.delete_if do |name|
152146
next true unless load_server_components
153147

lib/react_on_rails/react_component/render_options.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,17 @@ class RenderOptions
1515
def initialize(react_component_name: required("react_component_name"), options: required("options"))
1616
@react_component_name = react_component_name.camelize
1717
@options = options
18+
# The render_request_id serves as a unique identifier for each render request.
19+
# We cannot rely solely on dom_id, as it should be unique for each component on the page,
20+
# but the server can render the same page multiple times concurrently for different users.
21+
# Therefore, we need an additional unique identifier that can be used both on the client and server.
22+
# This ID can also be used to associate specific data with a particular rendered component
23+
# on either the server or client.
24+
# This ID is only present if RSC support is enabled because it's only used in that case.
25+
@render_request_id = self.class.generate_request_id if ReactOnRails::Utils.rsc_support_enabled?
1826
end
1927

20-
attr_reader :react_component_name
28+
attr_reader :react_component_name, :render_request_id
2129

2230
def throw_js_errors
2331
options.fetch(:throw_js_errors, false)
@@ -139,6 +147,10 @@ def store_dependencies
139147
options[:store_dependencies]
140148
end
141149

150+
def self.generate_request_id
151+
SecureRandom.uuid
152+
end
153+
142154
private
143155

144156
attr_reader :options

lib/react_on_rails/test_helper/webpack_assets_status_checker.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ def all_compiled_assets
5252
webpack_generated_files = @webpack_generated_files.map do |bundle_name|
5353
if bundle_name == ReactOnRails.configuration.react_client_manifest_file
5454
ReactOnRails::Utils.react_client_manifest_file_path
55+
elsif bundle_name == ReactOnRails.configuration.react_server_client_manifest_file
56+
ReactOnRails::Utils.react_server_client_manifest_file_path
5557
else
5658
ReactOnRails::Utils.bundle_js_file_path(bundle_name)
5759
end

0 commit comments

Comments
 (0)