Skip to content

Commit 6b2f6a2

Browse files
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
1 parent e43fd8d commit 6b2f6a2

Some content is hidden

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

49 files changed

+2088
-771
lines changed

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ const config = tsEslint.config([
161161
languageOptions: {
162162
parserOptions: {
163163
projectService: {
164-
allowDefaultProject: ['eslint.config.ts', 'knip.ts', 'node_package/tests/*.test.ts'],
164+
allowDefaultProject: ['eslint.config.ts', 'knip.ts', 'node_package/tests/*.test.{ts,tsx}'],
165165
// Needed because `import * as ... from` instead of `import ... from` doesn't work in this file
166166
// for some imports.
167167
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: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ 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!',
10+
'node_package/src/registerServerComponent/client.tsx!',
11+
'node_package/src/registerServerComponent/server.tsx!',
1212
'node_package/src/registerServerComponent/server.rsc.ts!',
13-
'node_package/src/RSCClientRoot.ts!',
13+
'node_package/src/wrapServerComponentRenderer/server.tsx!',
14+
'node_package/src/wrapServerComponentRenderer/server.rsc.tsx!',
15+
'node_package/src/RSCRoute.ts!',
1416
'eslint.config.ts',
1517
],
1618
project: ['node_package/src/**/*.[jt]s{x,}!', 'node_package/tests/**/*.[jt]s{x,}'],

lib/react_on_rails/configuration.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,6 @@ def ensure_webpack_generated_files_exists
312312
react_client_manifest_file,
313313
react_server_client_manifest_file
314314
].compact_blank
315-
316-
self.webpack_generated_files = files
317315
end
318316

319317
def configure_skip_display_none_deprecation

lib/react_on_rails/helper.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -358,14 +358,10 @@ def json_safe_and_pretty(hash_or_string)
358358
# second parameter passed to both component and store Render-Functions.
359359
# This method can be called from views and from the controller, as `helpers.rails_context`
360360
#
361-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
361+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
362362
def rails_context(server_side: true)
363363
# ALERT: Keep in sync with node_package/src/types/index.ts for the properties of RailsContext
364364
@rails_context ||= begin
365-
rsc_url = if ReactOnRails::Utils.react_on_rails_pro?
366-
ReactOnRailsPro.configuration.rsc_payload_generation_url_path
367-
end
368-
369365
result = {
370366
componentRegistryTimeout: ReactOnRails.configuration.component_registry_timeout,
371367
railsEnv: Rails.env,
@@ -381,7 +377,10 @@ def rails_context(server_side: true)
381377
if ReactOnRails::Utils.react_on_rails_pro?
382378
result[:rorProVersion] = ReactOnRails::Utils.react_on_rails_pro_version
383379

384-
result[:rscPayloadGenerationUrl] = rsc_url if ReactOnRailsPro.configuration.enable_rsc_support
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
385384
end
386385

387386
if defined?(request) && request.present?
@@ -439,7 +438,7 @@ def load_pack_for_generated_component(react_component_name, render_options)
439438
append_stylesheet_pack_tag("generated/#{react_component_name}")
440439
end
441440

442-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
441+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
443442

444443
private
445444

@@ -651,7 +650,8 @@ def internal_react_component(react_component_name, options = {})
651650
"data-trace" => (render_options.trace ? true : nil),
652651
"data-dom-id" => render_options.dom_id,
653652
"data-store-dependencies" => render_options.store_dependencies&.to_json,
654-
"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)
655655

656656
if render_options.force_load
657657
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/utils.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ def self.react_server_client_manifest_file_path
128128
return @react_server_manifest_path if @react_server_manifest_path && !Rails.env.development?
129129

130130
asset_name = ReactOnRails.configuration.react_server_client_manifest_file
131+
if asset_name.nil?
132+
raise ReactOnRails::Error,
133+
"react_server_client_manifest_file is nil, ensure it is set in your configuration"
134+
end
135+
131136
@react_server_manifest_path = File.join(generated_assets_full_path, asset_name)
132137
end
133138

@@ -208,6 +213,15 @@ def self.react_on_rails_pro_version
208213
end
209214
end
210215

216+
def self.rsc_support_enabled?
217+
return false unless react_on_rails_pro?
218+
219+
return @rsc_support_enabled if defined?(@rsc_support_enabled)
220+
221+
rorp_config = ReactOnRailsPro.configuration
222+
@rsc_support_enabled = rorp_config.respond_to?(:enable_rsc_support) && rorp_config.enable_rsc_support
223+
end
224+
211225
def self.full_text_errors_enabled?
212226
ENV["FULL_TEXT_ERRORS"] == "true"
213227
end

node_package/src/ClientSideRenderer.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,18 @@ class ComponentRenderer {
8383
const { domNodeId } = this;
8484
const props = el.textContent !== null ? (JSON.parse(el.textContent) as Record<string, unknown>) : {};
8585
const trace = el.getAttribute('data-trace') === 'true';
86+
const renderRequestId = el.getAttribute('data-render-request-id');
87+
88+
// The renderRequestId is optional and only present when React Server Components (RSC) support is enabled.
89+
// When RSC is enabled, this ID helps track and associate server-rendered components with their client-side hydration.
90+
const componentSpecificRailsContext = renderRequestId
91+
? {
92+
...railsContext,
93+
componentSpecificMetadata: {
94+
renderRequestId,
95+
},
96+
}
97+
: railsContext;
8698

8799
try {
88100
const domNode = document.getElementById(domNodeId);
@@ -93,7 +105,7 @@ class ComponentRenderer {
93105
}
94106

95107
if (
96-
(await delegateToRenderer(componentObj, props, railsContext, domNodeId, trace)) ||
108+
(await delegateToRenderer(componentObj, props, componentSpecificRailsContext, domNodeId, trace)) ||
97109
// @ts-expect-error The state can change while awaiting delegateToRenderer
98110
this.state === 'unmounted'
99111
) {
@@ -108,7 +120,7 @@ class ComponentRenderer {
108120
props,
109121
domNodeId,
110122
trace,
111-
railsContext,
123+
railsContext: componentSpecificRailsContext,
112124
shouldHydrate,
113125
});
114126

0 commit comments

Comments
 (0)