Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
b63596a
add needed utils to use rsc payload on the server
AbanoubGhadban Mar 17, 2025
d9eec0d
Refactor component registration and rendering logic to support `serve…
AbanoubGhadban Mar 17, 2025
f659285
Revert "Refactor component registration and rendering logic to suppor…
AbanoubGhadban Mar 17, 2025
bec4fc8
Enhance ReactOnRails options management
AbanoubGhadban Mar 17, 2025
08fd7cf
Revert "Enhance ReactOnRails options management"
AbanoubGhadban Mar 17, 2025
9e962c3
add support for returning promise of react component from render func…
AbanoubGhadban Mar 18, 2025
6f27f4a
Update ReactOnRails configuration to rename server manifest file for …
AbanoubGhadban Mar 18, 2025
33c1db2
Refactor ReactOnRails to support React Server Components (RSC) regist…
AbanoubGhadban Mar 19, 2025
0d32623
embed rsc payload inside the html page
AbanoubGhadban Mar 26, 2025
6f6c8ab
linting
AbanoubGhadban Mar 27, 2025
874eac4
fix ts errors
AbanoubGhadban Mar 27, 2025
ec35ccb
tmp
AbanoubGhadban Mar 27, 2025
be6469f
linting
AbanoubGhadban Mar 27, 2025
1483b40
linting
AbanoubGhadban Mar 27, 2025
14bf1ba
Update RSCClientRoot and related files to use RSCPayloadChunk type fo…
AbanoubGhadban Apr 4, 2025
377be74
Reset render state after processing each chunk to prevent error carry…
AbanoubGhadban Apr 4, 2025
aecebd7
Update RailsContext type to make rscPayloadGenerationUrl optional
AbanoubGhadban Apr 4, 2025
ca4dbba
Fix formatting in ReactOnRails helper for consistency
AbanoubGhadban Apr 4, 2025
a9540e2
Update error handling in createPromiseResult to return errorRenderSta…
AbanoubGhadban Apr 6, 2025
4a9d7a0
Refactor RSCServerRoot to use RailsContext in generateRSCPayload and …
AbanoubGhadban Apr 7, 2025
74b0c11
Add .yalc directory to ESLint ignore patterns
AbanoubGhadban Apr 7, 2025
6765eae
update changelog
AbanoubGhadban Apr 7, 2025
44d3758
rename __FLIGHT_DATA to REACT_ON_RAILS_RSC_PAYLOAD for improved clari…
AbanoubGhadban Apr 9, 2025
db9d080
Enhance type safety in RSCClientRoot and RSCPayloadContainer by using…
AbanoubGhadban Apr 9, 2025
4709047
convert createElement calls to jsx
AbanoubGhadban Apr 9, 2025
7d2b0d9
rename transformRSCNodeStreamAndReplayConsoleLogs to transformRSCNode…
AbanoubGhadban Apr 9, 2025
4214a6e
Enhance RSCPayloadContainer documentation on escape sequences and mod…
AbanoubGhadban Apr 9, 2025
bec40aa
refactoring
AbanoubGhadban Apr 10, 2025
f65e96a
remove unnecessary keys and streamline component rendering, enhancing…
AbanoubGhadban Apr 10, 2025
564d1eb
Update import paths in server.rsc.ts to include file extensions for i…
AbanoubGhadban Apr 26, 2025
e43fd8d
Update import statement in server.ts to use .tsx extension for RSCSer…
AbanoubGhadban Apr 26, 2025
6b2f6a2
Add ability to render server components inside client components (add…
AbanoubGhadban Jun 7, 2025
6a82137
Enhance error handling and memory management in RSC payload processing
AbanoubGhadban Jun 9, 2025
680f01b
add a bility to refetch and retry fetching of server components
AbanoubGhadban Jun 9, 2025
7b2e1f1
Update import statements in client and server components to use .tsx …
AbanoubGhadban Jun 9, 2025
5f48cc7
removed the functionality of caching components, react already caches…
AbanoubGhadban Jun 9, 2025
b9954a2
delete unused file and update knip configs
AbanoubGhadban Jun 9, 2025
8345920
Refactor RSC handling to improve performance and error management
AbanoubGhadban Jun 11, 2025
f428d6b
Fix test expectations for fetch calls in registerServerComponent test…
AbanoubGhadban Jun 11, 2025
9cd81f9
Enhance error handling in server component rendering and RSC payload …
AbanoubGhadban Jun 11, 2025
ead4a00
Update react-on-rails-rsc dependency to version 19.0.1 in package.jso…
AbanoubGhadban Jun 13, 2025
9206e01
update react to v19.1.0
AbanoubGhadban Jun 16, 2025
8dbef72
Revert "update react to v19.1.0"
AbanoubGhadban Jun 16, 2025
c9c59f8
Update react-on-rails-rsc dependency to version 19.0.2 in package.jso…
AbanoubGhadban Jun 16, 2025
60de83c
ensure to send the rsc payload array initilization before the html chunk
AbanoubGhadban Jun 16, 2025
88c85b9
Update changelog.md
AbanoubGhadban Jun 16, 2025
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ After a release, please make sure to run `bundle exec rake update_changelog`. Th

Changes since the last non-beta release.

#### Improved

- Improved RSC rendering flow by eliminating double rendering of server components and reducing the number of HTTP requests.
- Updated communication protocol between Node Renderer and Rails to version 2.0.0 which supports the ability to upload multiple bundles at once.
- Introduced `RSCServerRoot` and `RSCPayloadContainer` components to enable server-side rendering (SSR) of server components using RSC payload, and to embed the RSC payload directly into the page.

[PR 1696](https://github.com/shakacode/react_on_rails/pull/1696) by [AbanoubGhadban](https://github.com/AbanoubGhadban).

#### Added

- 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).
Expand Down
10 changes: 9 additions & 1 deletion eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const config = tsEslint.config([
'**/node_modules/',
// fixtures
'**/fixtures/',
'**/.yalc/**/*',
]),
{
files: ['**/*.[jt]s', '**/*.[jt]sx', '**/*.[cm][jt]s'],
Expand Down Expand Up @@ -71,7 +72,7 @@ const config = tsEslint.config([
alias: [['Assets', './spec/dummy/client/app/assets']],

node: {
extensions: ['.js', '.jsx', '.ts', '.d.ts'],
extensions: ['.js', '.jsx', '.ts', '.tsx', '.d.ts'],
},
},
},
Expand All @@ -96,6 +97,7 @@ const config = tsEslint.config([
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],

Expand Down Expand Up @@ -128,6 +130,12 @@ const config = tsEslint.config([
'react/jsx-props-no-spreading': 'off',
'react/static-property-placement': 'off',
'jsx-a11y/anchor-is-valid': 'off',
'react/jsx-filename-extension': [
'error',
{
extensions: ['.jsx', '.tsx'],
},
],
},
},
{
Expand Down
1 change: 1 addition & 0 deletions knip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const config: KnipConfig = {
'node_package/src/ReactOnRailsRSC.ts!',
'node_package/src/registerServerComponent/client.ts!',
'node_package/src/registerServerComponent/server.ts!',
'node_package/src/registerServerComponent/server.rsc.ts!',
'node_package/src/RSCClientRoot.ts!',
'eslint.config.ts',
],
Expand Down
13 changes: 10 additions & 3 deletions lib/react_on_rails/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def self.configure

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

def self.configuration
Expand All @@ -21,6 +22,7 @@ def self.configuration
server_bundle_js_file: "",
rsc_bundle_js_file: "",
react_client_manifest_file: DEFAULT_REACT_CLIENT_MANIFEST_FILE,
react_server_client_manifest_file: DEFAULT_REACT_SERVER_CLIENT_MANIFEST_FILE,
Copy link
Contributor

Choose a reason for hiding this comment

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

Any changes to configuration should have matching documentation changes

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated configuration.md

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see any changes to configuration.md

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@Judahmeek please check again. I merged the PR that contains the changes into this PR.

prerender: false,
auto_load_bundle: false,
replay_console: true,
Expand Down Expand Up @@ -66,7 +68,7 @@ class Configuration
:same_bundle_for_client_and_server, :rendering_props_extension,
:make_generated_server_bundle_the_entrypoint,
:generated_component_packs_loading_strategy, :force_load, :rsc_bundle_js_file,
:react_client_manifest_file, :component_registry_timeout
:react_client_manifest_file, :react_server_client_manifest_file, :component_registry_timeout

# rubocop:disable Metrics/AbcSize
def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender: nil,
Expand All @@ -82,7 +84,8 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
i18n_dir: nil, i18n_yml_dir: nil, i18n_output_format: nil, i18n_yml_safe_load_options: nil,
random_dom_id: nil, server_render_method: nil, rendering_props_extension: nil,
components_subdirectory: nil, auto_load_bundle: nil, force_load: nil,
rsc_bundle_js_file: nil, react_client_manifest_file: nil, component_registry_timeout: nil)
rsc_bundle_js_file: nil, react_client_manifest_file: nil, react_server_client_manifest_file: nil,
component_registry_timeout: nil)
self.node_modules_location = node_modules_location.present? ? node_modules_location : Rails.root
self.generated_assets_dirs = generated_assets_dirs
self.generated_assets_dir = generated_assets_dir
Expand Down Expand Up @@ -112,6 +115,7 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
self.server_bundle_js_file = server_bundle_js_file
self.rsc_bundle_js_file = rsc_bundle_js_file
self.react_client_manifest_file = react_client_manifest_file
self.react_server_client_manifest_file = react_server_client_manifest_file
self.same_bundle_for_client_and_server = same_bundle_for_client_and_server
self.server_renderer_pool_size = self.development_mode ? 1 : server_renderer_pool_size
self.server_renderer_timeout = server_renderer_timeout # seconds
Expand Down Expand Up @@ -305,8 +309,11 @@ def ensure_webpack_generated_files_exists
"manifest.json",
server_bundle_js_file,
rsc_bundle_js_file,
react_client_manifest_file
react_client_manifest_file,
react_server_client_manifest_file
].compact_blank

self.webpack_generated_files = files
end

def configure_skip_display_none_deprecation
Expand Down
11 changes: 9 additions & 2 deletions lib/react_on_rails/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -358,10 +358,14 @@ def json_safe_and_pretty(hash_or_string)
# second parameter passed to both component and store Render-Functions.
# This method can be called from views and from the controller, as `helpers.rails_context`
#
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def rails_context(server_side: true)
# ALERT: Keep in sync with node_package/src/types/index.ts for the properties of RailsContext
@rails_context ||= begin
rsc_url = if ReactOnRails::Utils.react_on_rails_pro?
ReactOnRailsPro.configuration.rsc_payload_generation_url_path
end

result = {
componentRegistryTimeout: ReactOnRails.configuration.component_registry_timeout,
railsEnv: Rails.env,
Expand All @@ -373,8 +377,11 @@ def rails_context(server_side: true)
# TODO: v13 just use the version if existing
rorPro: ReactOnRails::Utils.react_on_rails_pro?
}

if ReactOnRails::Utils.react_on_rails_pro?
result[:rorProVersion] = ReactOnRails::Utils.react_on_rails_pro_version

result[:rscPayloadGenerationUrl] = rsc_url if ReactOnRailsPro.configuration.enable_rsc_support
end

if defined?(request) && request.present?
Expand Down Expand Up @@ -432,7 +439,7 @@ def load_pack_for_generated_component(react_component_name, render_options)
append_stylesheet_pack_tag("generated/#{react_component_name}")
end

# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

private

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ def all_compiled_assets
webpack_generated_files = @webpack_generated_files.map do |bundle_name|
if bundle_name == ReactOnRails.configuration.react_client_manifest_file
ReactOnRails::Utils.react_client_manifest_file_path
elsif bundle_name == ReactOnRails.configuration.react_server_client_manifest_file
ReactOnRails::Utils.react_server_client_manifest_file_path
else
ReactOnRails::Utils.bundle_js_file_path(bundle_name)
end
Expand Down
9 changes: 9 additions & 0 deletions lib/react_on_rails/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ def self.react_client_manifest_file_path
end
end

# React Server Manifest is generated by the server bundle.
# So, it will never be served from the dev server.
def self.react_server_client_manifest_file_path
return @react_server_manifest_path if @react_server_manifest_path && !Rails.env.development?

asset_name = ReactOnRails.configuration.react_server_client_manifest_file
@react_server_manifest_path = File.join(generated_assets_full_path, asset_name)
end

def self.running_on_windows?
(/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil
end
Expand Down
68 changes: 63 additions & 5 deletions node_package/src/RSCClientRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import * as ReactDOMClient from 'react-dom/client';
import { createFromReadableStream } from 'react-on-rails-rsc/client';
import { fetch } from './utils.ts';
import transformRSCStreamAndReplayConsoleLogs from './transformRSCStreamAndReplayConsoleLogs.ts';
import { RailsContext, RenderFunction } from './types/index.ts';
import { RailsContext, RenderFunction, RSCPayloadChunk } from './types/index.ts';
import { ensureReactUseAvailable } from './reactApis.cts';

const { use } = React;
ensureReactUseAvailable();

if (typeof use !== 'function') {
throw new Error('React.use is not defined. Please ensure you are using React 19 to use server components.');
declare global {
interface Window {
REACT_ON_RAILS_RSC_PAYLOAD?: RSCPayloadChunk[];
}
}

export type RSCClientRootProps = {
Expand All @@ -35,6 +38,60 @@ const fetchRSC = ({ componentName, rscPayloadGenerationUrlPath, componentProps }
return createFromFetch(fetch(`/${strippedUrlPath}/${componentName}?props=${propsString}`));
};

const createRSCStreamFromPage = () => {
let streamController: ReadableStreamController<RSCPayloadChunk> | undefined;
const stream = new ReadableStream<RSCPayloadChunk>({
start(controller) {
if (typeof window === 'undefined') {
return;
}
const handleChunk = (chunk: RSCPayloadChunk) => {
controller.enqueue(chunk);
};

// The RSC payload transfer mechanism works in two possible scenarios:
// 1. RSCClientRoot executes first:
// - Initializes REACT_ON_RAILS_RSC_PAYLOAD as an empty array
// - Overrides the push function to handle incoming chunks
// - When server scripts run later, they use the overridden push function
// 2. Server scripts execute first:
// - Initialize REACT_ON_RAILS_RSC_PAYLOAD as an empty array
// - Buffer RSC payload chunks in the array
// - When RSCClientRoot runs, it reads buffered chunks and overrides push
//
// Key points:
// - The array is never reassigned, ensuring data consistency
// - The push function override ensures all chunks are properly handled
// - Execution order is irrelevant - both scenarios work correctly
if (!window.REACT_ON_RAILS_RSC_PAYLOAD) {
window.REACT_ON_RAILS_RSC_PAYLOAD = [];
}
window.REACT_ON_RAILS_RSC_PAYLOAD.forEach(handleChunk);
window.REACT_ON_RAILS_RSC_PAYLOAD.push = (...chunks) => {
chunks.forEach(handleChunk);
return chunks.length;
};
streamController = controller;
},
});

if (typeof document !== 'undefined' && document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
streamController?.close();
});
} else {
streamController?.close();
}

return stream;
};

const createFromRSCStream = () => {
const stream = createRSCStreamFromPage();
const transformedStream = transformRSCStreamAndReplayConsoleLogs(stream);
return createFromReadableStream<React.ReactNode>(transformedStream);
};

/**
* RSCClientRoot is a React component that handles client-side rendering of React Server Components (RSC).
* It manages the fetching, caching, and rendering of RSC payloads from the server.
Expand All @@ -53,7 +110,6 @@ const RSCClientRoot: RenderFunction = async (
_railsContext?: RailsContext,
domNodeId?: string,
) => {
const root = await fetchRSC({ componentName, rscPayloadGenerationUrlPath, componentProps });
if (!domNodeId) {
throw new Error('RSCClientRoot: No domNodeId provided');
}
Expand All @@ -62,8 +118,10 @@ const RSCClientRoot: RenderFunction = async (
throw new Error(`RSCClientRoot: No DOM node found for id: ${domNodeId}`);
}
if (domNode.innerHTML) {
const root = await createFromRSCStream();
ReactDOMClient.hydrateRoot(domNode, root);
} else {
const root = await fetchRSC({ componentName, rscPayloadGenerationUrlPath, componentProps });
ReactDOMClient.createRoot(domNode).render(root);
}
// Added only to satisfy the return type of RenderFunction
Expand Down
Loading
Loading