Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
9 changes: 5 additions & 4 deletions docs/guides/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,11 @@ ReactOnRails.configure do |config|
# See [15.0.0 Release Notes](docs/release-notes/15.0.0.md) for more details.
# config.defer_generated_component_packs = false

# Default is true
# When true, components hydrate immediately as soon as their server-rendered HTML reaches the client,
# without waiting for the full page load. This improves time-to-interactive performance.
config.force_load = true
# Default is false
# React on Rails Pro (licensed) feature: When true, components hydrate immediately as soon as
# their server-rendered HTML reaches the client, without waiting for the full page load.
# This improves time-to-interactive performance.
config.immediate_hydration = false

################################################################################
# I18N OPTIONS
Expand Down
2 changes: 1 addition & 1 deletion docs/rails/turbolinks.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ document.addEventListener('turbolinks:load', function () {
React on Rails 15 fixes both issues, so if you still have the listener it can be removed (and should be as `reactOnRailsPageLoaded()` is now async).

> [!WARNING]
> Do not use `force_load: false` with Turbolinks if you have async scripts.
> Do not use `immediate_hydration: false` (React on Rails Pro licensed feature) with Turbolinks if you have async scripts.

## Troubleshooting

Expand Down
14 changes: 7 additions & 7 deletions docs/release-notes/15.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ _The image above demonstrates the dramatic performance improvement:_

- The `defer_generated_component_packs` configuration has been deprecated. Use `generated_component_packs_loading_strategy` instead.
- The `generated_component_packs_loading_strategy` defaults to `:async` for Shakapacker ≥ 8.2.0 and `:sync` for Shakapacker < 8.2.0.
- The `force_load` configuration now defaults to `true`.
- The new default values of `generated_component_packs_loading_strategy: :async` and `force_load: true` work together to optimize component hydration. Components now hydrate as soon as their code and server-rendered HTML are available, without waiting for the full page to load. This parallel processing significantly improves time-to-interactive by eliminating the traditional waterfall of waiting for page load before beginning hydration (It's critical for streamed HTML).
- The `immediate_hydration` configuration now defaults to `false`. **Note: `immediate_hydration` is a React on Rails Pro (licensed) feature.**
- When `generated_component_packs_loading_strategy: :async` and `immediate_hydration: true` are configured together, they optimize component hydration. Components hydrate as soon as their code and server-rendered HTML are available, without waiting for the full page to load. This parallel processing significantly improves time-to-interactive by eliminating the traditional waterfall of waiting for page load before beginning hydration (It's critical for streamed HTML).

- 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.
- The `force_load` configuration makes `react-on-rails` hydrate components immediately as soon as their server-rendered HTML reaches the client, without waiting for the full page load.
- If you want to keep the previous behavior, you can set `generated_component_packs_loading_strategy: :defer` or `force_load: false` in your `config/initializers/react_on_rails.rb` file.
- You can also keep it for individual components by passing `force_load: false` to `react_component` or `stream_react_component`.
- Redux store now supports `force_load` option, which defaults to `config.force_load` (and so to `true` if that isn't set). If `true`, 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 calling the `redux_store` helper with `force_load: false`, same as `react_component`.
- The `immediate_hydration` configuration (React on Rails Pro licensed feature) makes `react-on-rails` hydrate components immediately as soon as their server-rendered HTML reaches the client, without waiting for the full page load.
- To enable optimized hydration, you can set `immediate_hydration: true` in your `config/initializers/react_on_rails.rb` file (requires React on Rails Pro license).
- You can also enable it for individual components by passing `immediate_hydration: true` to `react_component` or `stream_react_component`.
- Redux store now supports the `immediate_hydration` option (React on Rails Pro licensed feature), which defaults to `config.immediate_hydration` (and so to `false` if that isn't set). If `true`, 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 calling the `redux_store` helper with `immediate_hydration: true` or `immediate_hydration: false`, same as `react_component`.

- `ReactOnRails.reactOnRailsPageLoaded()` is now an async function:

Expand Down
10 changes: 5 additions & 5 deletions lib/react_on_rails/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ def self.configuration
components_subdirectory: nil,
make_generated_server_bundle_the_entrypoint: false,
defer_generated_component_packs: false,
# forces the loading of React components
force_load: true,
# React on Rails Pro (licensed) feature - enables immediate hydration of React components
immediate_hydration: false,
# Maximum time in milliseconds to wait for client-side component registration after page load.
# If exceeded, an error will be thrown for server-side rendered components not registered on the client.
# Set to 0 to disable the timeout and wait indefinitely for component registration.
Expand All @@ -67,7 +67,7 @@ class Configuration
:server_render_method, :random_dom_id, :auto_load_bundle,
: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,
:generated_component_packs_loading_strategy, :immediate_hydration, :rsc_bundle_js_file,
:react_client_manifest_file, :react_server_client_manifest_file, :component_registry_timeout

# rubocop:disable Metrics/AbcSize
Expand All @@ -83,7 +83,7 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
same_bundle_for_client_and_server: nil,
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,
components_subdirectory: nil, auto_load_bundle: nil, immediate_hydration: 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
Expand Down Expand Up @@ -128,7 +128,7 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
self.auto_load_bundle = auto_load_bundle
self.make_generated_server_bundle_the_entrypoint = make_generated_server_bundle_the_entrypoint
self.defer_generated_component_packs = defer_generated_component_packs
self.force_load = force_load
self.immediate_hydration = immediate_hydration
self.generated_component_packs_loading_strategy = generated_component_packs_loading_strategy
end
# rubocop:enable Metrics/AbcSize
Expand Down
8 changes: 5 additions & 3 deletions lib/react_on_rails/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ module Controller
# JavaScript code.
# props: Named parameter props which is a Ruby Hash or JSON string which contains the properties
# to pass to the redux store.
# immediate_hydration: React on Rails Pro (licensed) feature. Pass as true if you wish to hydrate this
# store immediately instead of waiting for the page to load.
#
# Be sure to include view helper `redux_store_hydration_data` at the end of your layout or view
# or else there will be no client side hydration of your stores.
def redux_store(store_name, props: {}, force_load: nil)
force_load = ReactOnRails.configuration.force_load if force_load.nil?
def redux_store(store_name, props: {}, immediate_hydration: nil)
immediate_hydration = ReactOnRails.configuration.immediate_hydration if immediate_hydration.nil?
redux_store_data = { store_name: store_name,
props: props,
force_load: force_load }
immediate_hydration: immediate_hydration }
@registered_stores_defer_render ||= []
Comment on lines +17 to 22
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Parallellize with helper gating or centralize.

Helper gates and badges Pro-only options; controller path does not. Either invoke the same gating here or ensure only helper.rb’s path adds immediate hydration.

Run this to confirm there’s no second gating for controller-registered stores:


🏁 Script executed:

#!/bin/bash
# Look for pro gating around controller redux_store path
rg -n -C2 'registered_stores_defer_render|redux_store_data|immediate_hydration' lib | sed -n '1,200p'

Length of output: 13417


Gate immediate_hydration in the controller or centralize its handling

Controller path (lib/react_on_rails/controller.rb — def redux_store, ~lines 17–23) uses ReactOnRails.configuration.immediate_hydration and appends redux_store_data to @registered_stores_defer_render without calling support_pro_features? or emitting the Pro badge; helper path (lib/react_on_rails/helper.rb — redux_store and pro_warning_badge_if_needed) does enforce gating. Either add the same gating in the controller (call pro_warning_badge_if_needed and set immediate_hydration = false unless support_pro_features?) or consolidate immediate_hydration normalization into a single shared method used by both paths.

🤖 Prompt for AI Agents
In lib/react_on_rails/controller.rb around lines 17 to 22, the controller path
uses ReactOnRails.configuration.immediate_hydration and registers
redux_store_data without applying the Pro-feature gating that the helper path
enforces; update the controller to either (a) call the same
pro_warning_badge_if_needed helper and set immediate_hydration = false unless
support_pro_features? before building redux_store_data, or (b) move the
immediate_hydration normalization/gating into a shared private method used by
both controller and helper and call that here so immediate_hydration is always
falsified when Pro features are unavailable and the Pro badge/warning is emitted
consistently.

@registered_stores_defer_render << redux_store_data
end
Expand Down
59 changes: 37 additions & 22 deletions lib/react_on_rails/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ module Helper
include ReactOnRails::Utils::Required

COMPONENT_HTML_KEY = "componentHtml"
IMMEDIATE_HYDRATION_PRO_WARNING = "[REACT ON RAILS] The 'immediate_hydration' feature requires a " \
"React on Rails Pro license. " \
"Please visit https://shakacode.com/react-on-rails-pro to learn more."

# react_component_name: can be a React function or class component or a "Render-Function".
# "Render-Functions" differ from a React function in that they take two parameters, the
Expand Down Expand Up @@ -58,7 +61,7 @@ def react_component(component_name, options = {})
server_rendered_html = internal_result[:result]["html"]
console_script = internal_result[:result]["consoleReplayScript"]
render_options = internal_result[:render_options]
badge = pro_warning_badge_if_needed(render_options.force_load)
badge = pro_warning_badge_if_needed(internal_result[:immediate_hydration_requested])

case server_rendered_html
when String
Expand Down Expand Up @@ -128,7 +131,7 @@ def stream_react_component(component_name, options = {})
# stream_react_component doesn't have the prerender option
# Because setting prerender to false is equivalent to calling react_component with prerender: false
options[:prerender] = true
options = options.merge(force_load: true) unless options.key?(:force_load)
options = options.merge(immediate_hydration: true) unless options.key?(:immediate_hydration)
run_stream_inside_fiber do
internal_stream_react_component(component_name, options)
end
Expand Down Expand Up @@ -210,11 +213,12 @@ def rsc_payload_react_component(component_name, options = {})
#
def react_component_hash(component_name, options = {})
options[:prerender] = true

internal_result = internal_react_component(component_name, options)
server_rendered_html = internal_result[:result]["html"]
console_script = internal_result[:result]["consoleReplayScript"]
render_options = internal_result[:render_options]
badge = pro_warning_badge_if_needed(render_options.force_load)
badge = pro_warning_badge_if_needed(internal_result[:immediate_hydration_requested])

if server_rendered_html.is_a?(String) && internal_result[:result]["hasErrors"]
server_rendered_html = { COMPONENT_HTML_KEY => internal_result[:result]["html"] }
Expand Down Expand Up @@ -252,15 +256,16 @@ def react_component_hash(component_name, options = {})
# props: Ruby Hash or JSON string which contains the properties to pass to the redux store.
# Options
# defer: false -- pass as true if you wish to render this below your component.
# force_load: false -- pass as true if you wish to hydrate this store immediately instead of
# waiting for the page to load.
def redux_store(store_name, props: {}, defer: false, force_load: nil)
force_load = ReactOnRails.configuration.force_load if force_load.nil?
badge = pro_warning_badge_if_needed(force_load)
# immediate_hydration: false -- React on Rails Pro (licensed) feature. Pass as true if you wish to
# hydrate this store immediately instead of waiting for the page to load.
def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil)
immediate_hydration = ReactOnRails.configuration.immediate_hydration if immediate_hydration.nil?
badge = pro_warning_badge_if_needed(immediate_hydration)
immediate_hydration = false unless support_pro_features?

redux_store_data = { store_name: store_name,
props: props,
force_load: force_load }
immediate_hydration: immediate_hydration }
if defer
registered_stores_defer_render << redux_store_data
"YOU SHOULD NOT SEE THIS ON YOUR VIEW -- Uses as a code block, like <% redux_store %> " \
Expand Down Expand Up @@ -447,16 +452,20 @@ def load_pack_for_generated_component(react_component_name, render_options)

# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity

def pro_warning_badge_if_needed(force_load)
return "".html_safe unless force_load
return "".html_safe if ReactOnRails::Utils.react_on_rails_pro_licence_valid?
# Checks if React on Rails Pro features are available
# @return [Boolean] true if Pro license is valid, false otherwise
def support_pro_features?
ReactOnRails::Utils.react_on_rails_pro_licence_valid?
end

def pro_warning_badge_if_needed(immediate_hydration)
return "".html_safe unless immediate_hydration
return "".html_safe if support_pro_features?

warning_message = "[REACT ON RAILS] The 'force_load' feature requires a React on Rails Pro license. " \
"Please visit https://shakacode.com/react-on-rails-pro to learn more."
puts warning_message
Rails.logger.warn warning_message
puts IMMEDIATE_HYDRATION_PRO_WARNING
Rails.logger.warn IMMEDIATE_HYDRATION_PRO_WARNING

tooltip_text = "The 'force_load' feature requires a React on Rails Pro license. Click to learn more."
tooltip_text = "The 'immediate_hydration' feature requires a React on Rails Pro license. Click to learn more."

badge_html = <<~HTML
<a href="https://shakacode.com/react-on-rails-pro" target="_blank" rel="noopener noreferrer" title="#{tooltip_text}">
Expand Down Expand Up @@ -666,6 +675,9 @@ def internal_react_component(react_component_name, options = {})
# server has already rendered the HTML.

render_options = create_render_options(react_component_name, options)
# Capture the originally requested value so we can show a badge while still disabling the feature.
immediate_hydration_requested = render_options.immediate_hydration
render_options.set_option(:immediate_hydration, false) unless support_pro_features?

# Setup the page_loaded_js, which is the same regardless of prerendering or not!
# The reason is that React is smart about not doing extra work if the server rendering did its job.
Expand All @@ -678,9 +690,10 @@ def internal_react_component(react_component_name, options = {})
"data-trace" => (render_options.trace ? true : nil),
"data-dom-id" => render_options.dom_id,
"data-store-dependencies" => render_options.store_dependencies&.to_json,
"data-force-load" => (render_options.force_load ? true : nil))
"data-immediate-hydration" =>
(render_options.immediate_hydration ? true : nil))

if render_options.force_load
if render_options.immediate_hydration
component_specification_tag.concat(
content_tag(:script, %(
typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
Expand All @@ -695,7 +708,8 @@ def internal_react_component(react_component_name, options = {})
{
render_options: render_options,
tag: component_specification_tag,
result: result
result: result,
immediate_hydration_requested: immediate_hydration_requested
}
end

Expand All @@ -704,9 +718,10 @@ def render_redux_store_data(redux_store_data)
json_safe_and_pretty(redux_store_data[:props]).html_safe,
type: "application/json",
"data-js-react-on-rails-store" => redux_store_data[:store_name].html_safe,
"data-force-load" => (redux_store_data[:force_load] ? true : nil))
"data-immediate-hydration" =>
(redux_store_data[:immediate_hydration] ? true : nil))

if redux_store_data[:force_load]
if redux_store_data[:immediate_hydration]
store_hydration_data.concat(
content_tag(:script, <<~JS.strip_heredoc.html_safe
typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsStoreLoaded('#{redux_store_data[:store_name]}');
Expand Down
4 changes: 2 additions & 2 deletions lib/react_on_rails/react_component/render_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ def logging_on_server
retrieve_configuration_value_for(:logging_on_server)
end

def force_load
retrieve_configuration_value_for(:force_load)
def immediate_hydration
retrieve_configuration_value_for(:immediate_hydration)
end

def to_s
Expand Down
19 changes: 19 additions & 0 deletions node_package/src/ClientSideRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ import reactHydrateOrRender from './reactHydrateOrRender.ts';
import { debugTurbolinks } from './turbolinksUtils.ts';
import * as StoreRegistry from './StoreRegistry.ts';
import * as ComponentRegistry from './ComponentRegistry.ts';
import { onPageLoaded } from './pageLifecycle.ts';

const REACT_ON_RAILS_STORE_ATTRIBUTE = 'data-js-react-on-rails-store';
const IMMEDIATE_HYDRATION_PRO_WARNING =
"[REACT ON RAILS] The 'immediate_hydration' feature requires a React on Rails Pro license. " +
'Please visit https://shakacode.com/react-on-rails-pro to get a license.';

async function delegateToRenderer(
componentObj: RegisteredComponent,
Expand Down Expand Up @@ -78,6 +82,21 @@ class ComponentRenderer {
* delegates to a renderer registered by the user.
*/
private async render(el: Element, railsContext: RailsContext): Promise<void> {
const isImmediateHydrationRequested = el.getAttribute('data-immediate-hydration') === 'true';
const hasProLicense = railsContext.rorPro;

// Handle immediate_hydration feature usage without Pro license
if (isImmediateHydrationRequested && !hasProLicense) {
console.warn(IMMEDIATE_HYDRATION_PRO_WARNING);

// Fallback to standard behavior: wait for page load before hydrating
if (document.readyState === 'loading') {
await new Promise<void>((resolve) => {
onPageLoaded(resolve);
});
}
}

// This must match lib/react_on_rails/helper.rb
const name = el.getAttribute('data-component-name') || '';
const { domNodeId } = this;
Expand Down
3 changes: 2 additions & 1 deletion spec/dummy/spec/support/selenium_logger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
err_msg.include?("Timed out receiving message from renderer: 0.100") ||
err_msg.include?("SharedArrayBuffer will require cross-origin isolation") ||
err_msg.include?("You are currently using minified code outside of NODE_ENV === \\\"production\\\"") ||
err_msg.include?("This version of ChromeDriver has not been tested with Chrome version")
err_msg.include?("This version of ChromeDriver has not been tested with Chrome version") ||
err_msg.include?("The 'immediate_hydration' feature requires a React on Rails Pro license")
end

raise("Java Script Error(s) on the page:\n\n#{clean_errors.join("\n")}") if clean_errors.present?
Expand Down
Loading
Loading