Skip to content

Commit 4dee1ff

Browse files
Add support for RSC (#1644)
* hydrate the component immediately when loaded and registered * auto register server components and immediately hydrate stores * move react-server-dom-webpack.d.ts to types directory * ensure to initialize registered stores array before accessing * refactor registration callback into separate class * make the early hydration compatible with turbopack, backward compatible and refactor * pass rsc path to RSC Client Root and move the config to RORP * update min node version to 18 * export registerServerComponent as a separate entrypoint to avoid client bundle increase * Update webpack assets status checker to use server bundle configuration * Update webpack assets status checker to handle bundle file paths * [WIP] handle errors happen in rsc payload (#1663) * stream rsc payload in json objects like streamed react components * make path to rsc bundle and react client manifest configurable * feat: Improve client manifest path handling for dev server - Add `dev_server_url` helper to centralize dev server URL construction - Add `public_output_uri_path` to get relative webpack output path - Add `asset_uri_from_packer` to handle asset URIs consistently - Update `react_client_manifest_file_path` to return dev server URLs when appropriate - Add comprehensive specs for new asset URI handling This change ensures client manifest paths are properly resolved to dev server URLs during development, improving hot-reloading functionality. * fix: normalize RSC URL path by absorbing leading/trailing slashes * specify Shakapacker as top-level module * add tests for RSCClientRoot * Make RSCClientRoot tests run with react 18 * Update webpack asset path configuration for client manifest * Fix client startup rendering when the script runs after the page loaded * Refactor client-side rendering and page lifecycle management - Extract context and page lifecycle utilities into separate modules - Improve handling of Turbolinks and Turbo events - Simplify client startup and rendering process - Add more robust page load and unload event management - Rename and reorganize context-related functions * Add component registry timeout configuration - Introduce a new configuration option `component_registry_timeout` to control the maximum time to wait for client-side component registration - Update `CallbackRegistry` to handle timeout events and reject pending callbacks - Add validation for the timeout configuration in the Rails configuration - Modify client-side startup and page lifecycle to support the new timeout mechanism - Enhance error handling for unregistered components and stores * Refactor CallbackRegistry and clientStartup initialization - Move `initializeTimeoutEvents()` call to `getOrWaitForItem()` method in CallbackRegistry - Remove unnecessary 4-second delay in clientStartup - Clean up commented-out webpack configuration in clientWebpackConfig * Update StoreRegistry error messages for clarity * Fix RSC stream parsing to handle incomplete chunks - Introduce `lastIncompleteChunk` to preserve partial JSON data between stream reads - Ensure complete JSON chunks are processed by splitting on newlines - Handle cases where the last chunk is not terminated with a newline * Refactor CallbackRegistry to improve item tracking and usage - Introduce `ItemInfo` type to track item registration, usage, and promise details - Enhance timeout handling with more informative warnings for unused items - Improve `get`, `has`, and `getAll` methods to better track item usage - Optimize `getOrWaitForItem` to handle existing promises and registration states * Refactor CallbackRegistry and ComponentRegistry to simplify item tracking - Simplify CallbackRegistry by separating item storage and waiting promises - Introduce separate maps for registered items and waiting promises - Add a set to track unused items - Update ComponentRegistry to include a clear method - Modify test to use new clear method * Update StoreRegistry test to use clearHydratedStores method * Update RSC test to append newline to stream chunks * don't strip the html chunk * Add "use client" directive to RSCClientRoot * Convert RSCWebpackLoader to TypeScript * Comment on the need for workaround * Fix Knip * Simplify RSCWebpackLoader * remove rsc? and stream? render options and add render_mode option * Rename flight_payload_streaming to rsc_payload_streaming * Add async component retrieval and timeout handling to ComponentRegistry * add specs for packs generator * Update react_on_rails_helper_spec with new props added to component definition script * Remove webpacker dependency from Gemfile.lock * Remove unnecessary webpacker mocking in configuration spec * Update package.json exports for React Server Components * fix: don't trim html content on server * Enable prerendering by default for React Server Components * Rename RSC rendering methods and configuration to payload generation * add more comments for new components * linting * Update package.json exports order for React Server Components * remove prerender option for stream_react_component * small linting changes * Use node 16 to run oldes tests * Remove unnecessary data-store-dependencies attribute from test scripts * linting * Convert loadReactClientManifest to async and update RSC rendering * make RSCClientRoot tests compatible with React 19 * Test fetch function only with node version 18+ * pass props to RSC generator and avoid state reset on hydration * Add ignore configuration for Knip static analysis * small changes * Implement stream buffering to safely handle stream events and errors * Fix stream error emission in buffered stream * Simplify streaming result parsing logic in server rendering * remove mentions to experiment react 18 and use only react 19 * Refactor registerServerComponent into client and server modules * Add RSCWebpackPlugin for React Server Components * Replace react-server-dom-webpack with @shakacode-tools/react-on-rails-rsc * Update TypeScript configuration and package dependencies for React Server Components * Remove webpack dependency from package configuration * Update @shakacode-tools/react-on-rails-rsc dependency to latest commit * Make reactOnRailsPageLoaded async * Update configuration to enable force_load by default and modify redux_store helper * Bump version to 15.0.0-alpha.2 and update CHANGELOG * revert this: Update package to use @abanoubghadban/react-on-rails-rsc * Configure .npmrc for private GitHub package registry * Update import statements for path and fs modules in test file * Update test specs to add data-force-load attribute by default * Add null check for ReactOnRails global object in component and store loading scripts * Bump version to 15.0.0.alpha.2 * Update GitHub Actions workflows to use Shakacode Tools Packages Token * Update package to use @shakacode-tools/react-on-rails-rsc * Revert version to 14.2.0 and update CHANGELOG * Enhance server-side rendering error logging and debugging - Add full backtrace to error messages for better diagnostics - Add temporary debugging breakpoint with binding.pry * Remove temporary debugging breakpoint in server rendering * Revert gem version to 14.2.0 in Gemfile.lock * Remove backtrace from server rendering error logging * Update package to use react-on-rails-rsc package instead of the old private package * Update CHANGELOG and knip configuration - Add description for unreleased changes in CHANGELOG - Remove unused entry from knip configuration - Add jsdom as a development dependency in knip config * Change default defer behavior for generated component packs * Update test to reflect new default defer behavior for generated component packs * Update CHANGELOG and release notes for React on Rails 15.0.0 - Add comprehensive release notes for version 15.0.0 - Highlight major features: React Server Components support and improved component hydration - Document breaking changes related to component hydration and store dependencies - Summarize key improvements in component and store hydration performance * Add domNodeId support for server-side rendering identifier prefix to support multiple components in the view * Remove PR references from 15.0.0 release notes
1 parent a8b8c03 commit 4dee1ff

File tree

60 files changed

+3173
-628
lines changed

Some content is hidden

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

60 files changed

+3173
-628
lines changed

.github/workflows/rspec-package-specs.yml

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ jobs:
4747
git config user.email "[email protected]"
4848
git config user.name "Your Name"
4949
git commit -am "stop generators from complaining about uncommitted code"
50+
- name: Set packer version environment variable
51+
run: |
52+
echo "CI_PACKER_VERSION=${{ matrix.versions == 'oldest' && 'old' || 'new' }}" >> $GITHUB_ENV
5053
- name: Run rspec tests
5154
run: bundle exec rspec spec/react_on_rails
5255
- name: Store test results

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ Please follow the recommendations outlined at [keepachangelog.com](http://keepac
1818
### [Unreleased]
1919
Changes since the last non-beta release.
2020

21+
See [Release Notes](docs/release-notes/15.0.0.md) for full details.
22+
23+
#### Added
24+
- React Server Components Support (Pro Feature) [PR 1644](https://github.com/shakacode/react_on_rails/pull/1644) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
25+
- Improved component and store hydration performance [PR 1656](https://github.com/shakacode/react_on_rails/pull/1656) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
26+
27+
#### Breaking Changes
28+
- `ReactOnRails.reactOnRailsPageLoaded` is now an async function
29+
- `force_load` configuration now defaults to `true`
30+
- `defer_generated_component_packs` configuration now defaults to `false`
31+
2132
### [14.2.0] - 2025-03-03
2233

2334
#### Added

Gemfile.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
react_on_rails (14.1.1)
4+
react_on_rails (14.2.0)
55
addressable
66
connection_pool
77
execjs (~> 2.5)

docs/api/javascript-api.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ The best source of docs is the main [ReactOnRails.ts](https://github.com/shakaco
5959
getStore(name, throwIfMissing = true )
6060

6161
/**
62-
* Renders or hydrates the react element passed. In case react version is >=18 will use the new api.
62+
* Renders or hydrates the React element passed. In case React version is >=18 will use the root API.
6363
* @param domNode
6464
* @param reactElement
6565
* @param hydrate if true will perform hydration, if false will render

docs/guides/configuration.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,9 @@ ReactOnRails.configure do |config|
189189
# config.server_bundle_js_file for the filename.
190190
config.make_generated_server_bundle_the_entrypoint = false
191191

192-
# Default is true, which matches Webpacker/Shakapacker's defer default for `append_javascript_pack`
193-
# Set this to false to have `defer: false` added to your `append_javascript_pack` calls for generated entrypoints.
194-
config.defer_generated_component_packs = true
192+
# Default is false
193+
# Set this to true to have `defer: true` added to your `append_javascript_pack` calls for generated entrypoints.
194+
config.defer_generated_component_packs = false
195195

196196
################################################################################
197197
# I18N OPTIONS

docs/guides/streaming-server-rendering.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ React on Rails Pro supports streaming server rendering using React 18's latest A
55
## Prerequisites
66

77
- React on Rails Pro subscription
8-
- React 18 or higher (experimental version)
8+
- React 19
99
- React on Rails v15.0.0-alpha.0 or higher
1010
- React on Rails Pro v4.0.0.rc.5 or higher
1111

@@ -19,14 +19,14 @@ React on Rails Pro supports streaming server rendering using React 18's latest A
1919

2020
## Implementation Steps
2121

22-
1. **Use Experimental React 18 Version**
22+
1. **Use React 19 Version**
2323

24-
First, ensure you're using React 18's experimental version in your package.json:
24+
First, ensure you're using React 19 in your package.json:
2525

2626
```json
2727
"dependencies": {
28-
"react": "18.3.0-canary-670811593-20240322",
29-
"react-dom": "18.3.0-canary-670811593-20240322"
28+
"react": "19.0.0",
29+
"react-dom": "19.0.0"
3030
}
3131
```
3232

docs/release-notes/15.0.0.md

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# React on Rails 15.0.0 Release Notes
2+
3+
## Major Features
4+
5+
### 🚀 React Server Components Support
6+
Experience the future of React with full RSC integration in your Rails apps:
7+
- Seamlessly use React Server Components
8+
- Reduce client bundle sizes
9+
- Enable powerful new patterns for data fetching
10+
- ⚡️ Requires React on Rails Pro - [See the full tutorial](https://www.shakacode.com/react-on-rails-pro/docs/react-server-components-tutorial)
11+
12+
### Improved Component Hydration
13+
Major improvements to component and store hydration:
14+
- Components and stores now hydrate immediately rather than waiting for page load
15+
- Enables faster hydration, especially beneficial for streamed pages
16+
- Components can hydrate before the page is fully streamed
17+
- Can use `async` scripts in the page with no fear of race condition
18+
- No need to use `defer` anymore
19+
20+
## Breaking Changes
21+
22+
### Component Hydration Changes
23+
- The `defer_generated_component_packs` and `force_load` configurations now default to `false` and `true` respectively. This means components will hydrate early without waiting for the full page load. This improves performance by eliminating unnecessary delays in hydration.
24+
- The previous need for deferring scripts to prevent race conditions has been eliminated due to improved hydration handling. Making scripts not defer is critical to execute the hydration scripts early before the page is fully loaded.
25+
- The `force_load` configuration make `react-on-rails` hydrate components immediately as soon as their server-rendered HTML reaches the client, without waiting for the full page load.
26+
- If you want to keep the previous behavior, you can set `defer_generated_component_packs: true` or `force_load: false` in your `config/initializers/react_on_rails.rb` file.
27+
- If we want to keep the original behavior of `force_load` for only one or more components, you can set `force_load: false` in the `react_component` helper or `force_load` configuration.
28+
- Redux store support `force_load` option now and it uses `config.force_load` value as the default value. Which means that the redux store will hydrate immediately as soon as its server-side data reaches the client. You can override this behavior for individual redux stores by setting `force_load: false` in the `redux_store` helper.
29+
30+
- `ReactOnRails.reactOnRailsPageLoaded()` is now an async function:
31+
- If you are manually calling this function to ensure components are hydrated (e.g. with async script loading), you must now await the promise it returns:
32+
```js
33+
// Before
34+
ReactOnRails.reactOnRailsPageLoaded();
35+
// Code expecting all components to be hydrated
36+
37+
// After
38+
await ReactOnRails.reactOnRailsPageLoaded();
39+
// Code expecting all components to be hydrated
40+
```
41+
42+
## Store Dependencies for Components
43+
44+
When using Redux stores with multiple components, you need to explicitly declare store dependencies to optimize hydration. Here's how:
45+
46+
### The Problem
47+
48+
If you have deferred Redux stores and components like this:
49+
50+
```erb
51+
<% redux_store("SimpleStore", props: @app_props_server_render, defer: true) %>
52+
<%= react_component('ReduxApp', {}, {prerender: true}) %>
53+
<%= react_component('ComponentWithNoStore', {}, {prerender: true}) %>
54+
<%= redux_store_hydration_data %>
55+
```
56+
57+
By default, React on Rails assumes components depend on all previously created stores. This means:
58+
- Neither `ReduxApp` nor `ComponentWithNoStore` will hydrate until `SimpleStore` is hydrated
59+
- Since the store is deferred to the end of the page, both components are forced to wait unnecessarily
60+
61+
### The Solution
62+
63+
Explicitly declare store dependencies for each component:
64+
65+
```erb
66+
<% redux_store("SimpleStore", props: @app_props_server_render, defer: true) %>
67+
<%= react_component('ReduxApp', {}, {
68+
prerender: true
69+
<!-- No need to specify store_dependencies - it automatically depends on SimpleStore -->
70+
}) %>
71+
<%= react_component('ComponentWithNoStore', {}, {
72+
prerender: true,
73+
store_dependencies: [] <!-- Explicitly declare no store dependencies -->
74+
}) %>
75+
<%= redux_store_hydration_data %>
76+
```
77+
78+
This allows `ComponentWithNoStore` to hydrate immediately without waiting for `SimpleStore`, improving page performance.

jest.config.js

+12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
1+
const nodeVersion = parseInt(process.version.slice(1), 10);
2+
13
module.exports = {
24
preset: 'ts-jest/presets/js-with-ts',
35
testEnvironment: 'jsdom',
46
setupFiles: ['<rootDir>/node_package/tests/jest.setup.js'],
7+
// React Server Components tests are compatible with React 19
8+
// That only run with node version 18 and above
9+
moduleNameMapper:
10+
nodeVersion < 18
11+
? {
12+
'react-on-rails-rsc/client': '<rootDir>/node_package/tests/emptyForTesting.js',
13+
'^@testing-library/dom$': '<rootDir>/node_package/tests/emptyForTesting.js',
14+
'^@testing-library/react$': '<rootDir>/node_package/tests/emptyForTesting.js',
15+
}
16+
: {},
517
};

knip.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,20 @@ const config: KnipConfig = {
44
// ! at the end means files are used in production
55
workspaces: {
66
'.': {
7-
entry: ['node_package/src/ReactOnRails.ts!', 'node_package/src/ReactOnRails.node.ts!'],
8-
project: ['node_package/src/**/*.[jt]s!', 'node_package/tests/**/*.[jt]s'],
7+
entry: [
8+
'node_package/src/ReactOnRails.node.ts!',
9+
'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!',
13+
],
14+
project: ['node_package/src/**/*.[jt]s{x,}!', 'node_package/tests/**/*.[jt]s{x,}'],
915
babel: {
1016
config: ['node_package/babel.config.js'],
1117
},
18+
ignore: [
19+
'node_package/tests/emptyForTesting.js',
20+
],
1221
ignoreBinaries: [
1322
// Knip fails to detect it's declared in devDependencies
1423
'nps',
@@ -26,6 +35,8 @@ const config: KnipConfig = {
2635
'eslint-plugin-react',
2736
// Used in CI
2837
'@arethetypeswrong/cli',
38+
// used by Jest
39+
'jsdom',
2940
],
3041
},
3142
'spec/dummy': {

lib/react_on_rails/configuration.rb

+32-9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ def self.configure
99
end
1010

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

1315
def self.configuration
1416
@configuration ||= Configuration.new(
@@ -17,6 +19,8 @@ def self.configuration
1719
# generated_assets_dirs is deprecated
1820
generated_assets_dir: "",
1921
server_bundle_js_file: "",
22+
rsc_bundle_js_file: "",
23+
react_client_manifest_file: DEFAULT_REACT_CLIENT_MANIFEST_FILE,
2024
prerender: false,
2125
auto_load_bundle: false,
2226
replay_console: true,
@@ -39,9 +43,13 @@ def self.configuration
3943
i18n_output_format: nil,
4044
components_subdirectory: nil,
4145
make_generated_server_bundle_the_entrypoint: false,
42-
defer_generated_component_packs: true,
46+
defer_generated_component_packs: false,
4347
# forces the loading of React components
44-
force_load: false
48+
force_load: true,
49+
# Maximum time in milliseconds to wait for client-side component registration after page load.
50+
# If exceeded, an error will be thrown for server-side rendered components not registered on the client.
51+
# Set to 0 to disable the timeout and wait indefinitely for component registration.
52+
component_registry_timeout: DEFAULT_COMPONENT_REGISTRY_TIMEOUT
4553
)
4654
end
4755

@@ -56,8 +64,8 @@ class Configuration
5664
:server_render_method, :random_dom_id, :auto_load_bundle,
5765
:same_bundle_for_client_and_server, :rendering_props_extension,
5866
:make_generated_server_bundle_the_entrypoint,
59-
:defer_generated_component_packs,
60-
:force_load
67+
:defer_generated_component_packs, :force_load, :rsc_bundle_js_file,
68+
:react_client_manifest_file, :component_registry_timeout
6169

6270
# rubocop:disable Metrics/AbcSize
6371
def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender: nil,
@@ -72,7 +80,8 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
7280
same_bundle_for_client_and_server: nil,
7381
i18n_dir: nil, i18n_yml_dir: nil, i18n_output_format: nil, i18n_yml_safe_load_options: nil,
7482
random_dom_id: nil, server_render_method: nil, rendering_props_extension: nil,
75-
components_subdirectory: nil, auto_load_bundle: nil, force_load: nil)
83+
components_subdirectory: nil, auto_load_bundle: nil, force_load: nil,
84+
rsc_bundle_js_file: nil, react_client_manifest_file: nil, component_registry_timeout: nil)
7685
self.node_modules_location = node_modules_location.present? ? node_modules_location : Rails.root
7786
self.generated_assets_dirs = generated_assets_dirs
7887
self.generated_assets_dir = generated_assets_dir
@@ -96,9 +105,12 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
96105
self.raise_on_prerender_error = raise_on_prerender_error
97106
self.skip_display_none = skip_display_none
98107
self.rendering_props_extension = rendering_props_extension
108+
self.component_registry_timeout = component_registry_timeout
99109

100110
# Server rendering:
101111
self.server_bundle_js_file = server_bundle_js_file
112+
self.rsc_bundle_js_file = rsc_bundle_js_file
113+
self.react_client_manifest_file = react_client_manifest_file
102114
self.same_bundle_for_client_and_server = same_bundle_for_client_and_server
103115
self.server_renderer_pool_size = self.development_mode ? 1 : server_renderer_pool_size
104116
self.server_renderer_timeout = server_renderer_timeout # seconds
@@ -126,10 +138,19 @@ def setup_config_values
126138
error_if_using_packer_and_generated_assets_dir_not_match_public_output_path
127139
# check_deprecated_settings
128140
adjust_precompile_task
141+
check_component_registry_timeout
129142
end
130143

131144
private
132145

146+
def check_component_registry_timeout
147+
self.component_registry_timeout = DEFAULT_COMPONENT_REGISTRY_TIMEOUT if component_registry_timeout.nil?
148+
149+
return if component_registry_timeout.is_a?(Integer) && component_registry_timeout >= 0
150+
151+
raise ReactOnRails::Error, "component_registry_timeout must be a positive integer"
152+
end
153+
133154
def check_autobundling_requirements
134155
raise_missing_components_subdirectory if auto_load_bundle && !components_subdirectory.present?
135156
return unless components_subdirectory.present?
@@ -241,10 +262,12 @@ def configure_generated_assets_dirs_deprecation
241262
def ensure_webpack_generated_files_exists
242263
return unless webpack_generated_files.empty?
243264

244-
files = ["manifest.json"]
245-
files << server_bundle_js_file if server_bundle_js_file.present?
246-
247-
self.webpack_generated_files = files
265+
self.webpack_generated_files = [
266+
"manifest.json",
267+
server_bundle_js_file,
268+
rsc_bundle_js_file,
269+
react_client_manifest_file
270+
].compact_blank
248271
end
249272

250273
def configure_skip_display_none_deprecation

0 commit comments

Comments
 (0)