From cc6f86fd9a7d9bba54483186d3d6c2f73cc062d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Nov 2025 15:46:41 +0000 Subject: [PATCH 01/17] Remove pro_warning_badge helper methods and constant Remove the visual warning badge HTML generation code: - Remove IMMEDIATE_HYDRATION_PRO_WARNING constant - Remove pro_warning_badge_if_needed method - Remove disabled_pro_features_message method These methods generated a red banner in the corner of the page when non-pro users tried to enable pro-only features. This functionality is being removed as part of the plan to separate pro features from the core gem. The calls to pro_warning_badge_if_needed will be removed in subsequent commits. --- lib/react_on_rails/pro_helper.rb | 35 -------------------------------- 1 file changed, 35 deletions(-) diff --git a/lib/react_on_rails/pro_helper.rb b/lib/react_on_rails/pro_helper.rb index 41d0a51763..d8fb6b79e5 100644 --- a/lib/react_on_rails/pro_helper.rb +++ b/lib/react_on_rails/pro_helper.rb @@ -2,10 +2,6 @@ module ReactOnRails module ProHelper - 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." - # Generates the complete component specification script tag. # Handles both immediate hydration (Pro feature) and standard cases. def generate_component_script(render_options) @@ -71,36 +67,5 @@ def generate_store_script(redux_store_data) "#{pro_warning_badge}\n#{store_hydration_scripts}".html_safe end - def pro_warning_badge_if_needed(explicitly_disabled_pro_options) - return "" unless explicitly_disabled_pro_options.any? - - disabled_features_message = disabled_pro_features_message(explicitly_disabled_pro_options) - warning_message = "[REACT ON RAILS] #{disabled_features_message}\n" \ - "Please visit https://shakacode.com/react-on-rails-pro to learn more." - puts warning_message - Rails.logger.warn warning_message - - tooltip_text = "#{disabled_features_message} Click to learn more." - - <<~HTML.strip - -
-
- React On Rails Pro Required -
-
-
- HTML - end - - def disabled_pro_features_message(explicitly_disabled_pro_options) - return "".html_safe unless explicitly_disabled_pro_options.any? - - feature_list = explicitly_disabled_pro_options.join(", ") - feature_word = explicitly_disabled_pro_options.size == 1 ? "feature" : "features" - "The '#{feature_list}' #{feature_word} " \ - "#{explicitly_disabled_pro_options.size == 1 ? 'requires' : 'require'} a " \ - "React on Rails Pro license. " - end end end From bbc2de378d040227d11b9fbd1a2db25b12d259ab Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Nov 2025 15:47:12 +0000 Subject: [PATCH 02/17] Remove pro_warning_badge from generate_component_script Remove the call to pro_warning_badge_if_needed in the generate_component_script method. The method now returns the component specification tag directly without appending the warning badge HTML. This is part of removing the visual warning badge that appeared when non-pro users tried to use pro-only features. --- lib/react_on_rails/pro_helper.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/react_on_rails/pro_helper.rb b/lib/react_on_rails/pro_helper.rb index d8fb6b79e5..f3f066853a 100644 --- a/lib/react_on_rails/pro_helper.rb +++ b/lib/react_on_rails/pro_helper.rb @@ -32,8 +32,7 @@ def generate_component_script(render_options) component_specification_tag end - pro_warning_badge = pro_warning_badge_if_needed(render_options.explicitly_disabled_pro_options) - "#{pro_warning_badge}\n#{spec_tag}".html_safe + spec_tag.html_safe end # Generates the complete store hydration script tag. From 2daca1a271f93c8eeb1269103e08a876a20e28d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Nov 2025 15:47:47 +0000 Subject: [PATCH 03/17] Remove pro_warning_badge from generate_store_script Remove the call to pro_warning_badge_if_needed in the generate_store_script method and simplify the handling of the result from disable_pro_render_options_if_not_licensed. The method now: - Directly assigns the result without extracting sub-hashes - Returns the store hydration scripts without the warning badge This continues the removal of the visual warning badge for pro-only features. --- lib/react_on_rails/pro_helper.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/react_on_rails/pro_helper.rb b/lib/react_on_rails/pro_helper.rb index f3f066853a..f3dcac9608 100644 --- a/lib/react_on_rails/pro_helper.rb +++ b/lib/react_on_rails/pro_helper.rb @@ -38,9 +38,7 @@ def generate_component_script(render_options) # Generates the complete store hydration script tag. # Handles both immediate hydration (Pro feature) and standard cases. def generate_store_script(redux_store_data) - pro_options_check_result = ReactOnRails::ProUtils.disable_pro_render_options_if_not_licensed(redux_store_data) - redux_store_data = pro_options_check_result[:raw_options] - explicitly_disabled_pro_options = pro_options_check_result[:explicitly_disabled_pro_options] + redux_store_data = ReactOnRails::ProUtils.disable_pro_render_options_if_not_licensed(redux_store_data) store_hydration_data = content_tag(:script, json_safe_and_pretty(redux_store_data[:props]).html_safe, @@ -62,8 +60,7 @@ def generate_store_script(redux_store_data) store_hydration_data end - pro_warning_badge = pro_warning_badge_if_needed(explicitly_disabled_pro_options) - "#{pro_warning_badge}\n#{store_hydration_scripts}".html_safe + store_hydration_scripts.html_safe end end From 67dcc235fe2e635978fda09e0a68ede31b5848ea Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Nov 2025 15:48:20 +0000 Subject: [PATCH 04/17] Simplify ProUtils to return options directly Refactor disable_pro_render_options_if_not_licensed to: - Return raw options directly instead of a hash - No longer track explicitly_disabled_pro_options - Silently disable pro-only options when Pro is not available This simplification removes the need to track which options were disabled, as we no longer display a warning badge. The method now only disables pro-only features silently, which is the desired behavior going forward. --- lib/react_on_rails/pro_utils.rb | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/lib/react_on_rails/pro_utils.rb b/lib/react_on_rails/pro_utils.rb index bf19e9bf78..bb222630cd 100644 --- a/lib/react_on_rails/pro_utils.rb +++ b/lib/react_on_rails/pro_utils.rb @@ -11,27 +11,23 @@ def self.support_pro_features? end def self.disable_pro_render_options_if_not_licensed(raw_options) - if support_pro_features? - return { - raw_options: raw_options, - explicitly_disabled_pro_options: [] - } - end + return raw_options if support_pro_features? raw_options_after_disable = raw_options.dup - explicitly_disabled_pro_options = PRO_ONLY_OPTIONS.select do |option| - # Use global configuration if it's not overridden in the options - next ReactOnRails.configuration.send(option) if raw_options[option].nil? + PRO_ONLY_OPTIONS.each do |option| + # Determine if this option is enabled (either explicitly or via global config) + option_enabled = if raw_options[option].nil? + ReactOnRails.configuration.send(option) + else + raw_options[option] + end - raw_options[option] + # Silently disable the option if it's enabled but Pro is not available + raw_options_after_disable[option] = false if option_enabled end - explicitly_disabled_pro_options.each { |option| raw_options_after_disable[option] = false } - { - raw_options: raw_options_after_disable, - explicitly_disabled_pro_options: explicitly_disabled_pro_options - } + raw_options_after_disable end end end From 31501dc0cc3a958a87fd0092635498cdec72c9bd Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Nov 2025 15:48:50 +0000 Subject: [PATCH 05/17] Remove explicitly_disabled_pro_options from RenderOptions Simplify RenderOptions initialization by: - Removing @explicitly_disabled_pro_options instance variable - Removing explicitly_disabled_pro_options from attr_reader - Directly assigning the result of disable_pro_render_options_if_not_licensed This completes the removal of the tracking mechanism for disabled pro options, as this information is no longer needed without the warning badge. --- lib/react_on_rails/react_component/render_options.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/react_on_rails/react_component/render_options.rb b/lib/react_on_rails/react_component/render_options.rb index 3d9a4cdf05..b6bc5f5fe3 100644 --- a/lib/react_on_rails/react_component/render_options.rb +++ b/lib/react_on_rails/react_component/render_options.rb @@ -15,13 +15,10 @@ class RenderOptions # TODO: remove the required for named params def initialize(react_component_name: required("react_component_name"), options: required("options")) @react_component_name = react_component_name.camelize - - result = ReactOnRails::ProUtils.disable_pro_render_options_if_not_licensed(options) - @options = result[:raw_options] - @explicitly_disabled_pro_options = result[:explicitly_disabled_pro_options] + @options = ReactOnRails::ProUtils.disable_pro_render_options_if_not_licensed(options) end - attr_reader :react_component_name, :explicitly_disabled_pro_options + attr_reader :react_component_name def throw_js_errors options.fetch(:throw_js_errors, false) From 1d349cb83035dafb372c709bec46bde13da3261c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Nov 2025 15:55:11 +0000 Subject: [PATCH 06/17] Fix Rubocop violations after badge removal Auto-fix Rubocop offense: - Remove extra empty line at module body end All Rubocop checks now pass. --- lib/react_on_rails/pro_helper.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/react_on_rails/pro_helper.rb b/lib/react_on_rails/pro_helper.rb index f3dcac9608..3a8b58b3ee 100644 --- a/lib/react_on_rails/pro_helper.rb +++ b/lib/react_on_rails/pro_helper.rb @@ -62,6 +62,5 @@ def generate_store_script(redux_store_data) store_hydration_scripts.html_safe end - end end From 105e4b5153ca79b8537d6870c36ee90004d62513 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Nov 2025 16:10:04 +0000 Subject: [PATCH 07/17] Remove obsolete pro_warning_badge tests Remove test contexts that verified the warning badge HTML and logging behavior: - Remove 'with Pro license warning' contexts from react_component tests - Remove 'with Pro license warning' contexts from react_component_hash tests - Remove 'with Pro license warning' contexts from redux_store tests These tests are no longer needed since the badge functionality has been removed. Pro features are now silently disabled when Pro is not installed, without displaying warnings or badges. Total: ~90 lines removed from test file --- .../helpers/react_on_rails_helper_spec.rb | 152 ------------------ 1 file changed, 152 deletions(-) diff --git a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb index 1a691fdc38..3b36f0e362 100644 --- a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb +++ b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb @@ -392,78 +392,6 @@ def helper.append_javascript_pack_tag(name, **options) it { is_expected.to include immediate_hydration_script } end end - - describe "with Pro license warning" do - let(:badge_html_string) { "React On Rails Pro Required" } - - before do - allow(Rails.logger).to receive(:warn) - end - - context "when Pro license is NOT installed and immediate_hydration is true" do - subject(:react_app) { react_component("App", props: props, immediate_hydration: true) } - - before do - allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(false) - end - - it { is_expected.to include(badge_html_string) } - - it "logs a warning" do - react_app - expect(Rails.logger).to have_received(:warn) - .with(a_string_matching(/The 'immediate_hydration' feature requires/)) - end - end - - context "when Pro license is NOT installed and global immediate_hydration is true" do - subject(:react_app) { react_component("App", props: props) } - - before do - allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(false) - end - - around do |example| - ReactOnRails.configure { |config| config.immediate_hydration = true } - example.run - ReactOnRails.configure { |config| config.immediate_hydration = false } - end - - it { is_expected.to include(badge_html_string) } - end - - context "when Pro license is NOT installed and immediate_hydration is false" do - subject(:react_app) { react_component("App", props: props, immediate_hydration: false) } - - before do - allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(false) - end - - it { is_expected.not_to include(badge_html_string) } - - it "does not log a warning" do - react_app - expect(Rails.logger).not_to have_received(:warn) - end - end - - context "when Pro license IS installed and immediate_hydration is true" do - subject(:react_app) { react_component("App", props: props, immediate_hydration: true) } - - before do - allow(ReactOnRails::Utils).to receive_messages( - react_on_rails_pro?: true - ) - end - - it { is_expected.not_to include(badge_html_string) } - - it "does not log a warning" do - react_app - expect(Rails.logger).not_to have_received(:warn) - end - end - end end describe "#react_component_hash" do @@ -486,40 +414,6 @@ def helper.append_javascript_pack_tag(name, **options) expect(react_app).to have_key("componentHtml") expect(react_app).to have_key("title") end - - context "with Pro license warning" do - let(:badge_html_string) { "React On Rails Pro Required" } - - before do - allow(Rails.logger).to receive(:warn) - end - - context "when Pro license is NOT installed and immediate_hydration is true" do - subject(:react_app) { react_component_hash("App", props: props, immediate_hydration: true) } - - before do - allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(false) - end - - it "adds badge to componentHtml" do - expect(react_app["componentHtml"]).to include(badge_html_string) - end - end - - context "when Pro license IS installed and immediate_hydration is true" do - subject(:react_app) { react_component_hash("App", props: props, immediate_hydration: true) } - - before do - allow(ReactOnRails::Utils).to receive_messages( - react_on_rails_pro?: true - ) - end - - it "does not add badge to componentHtml" do - expect(react_app["componentHtml"]).not_to include(badge_html_string) - end - end - end end describe "#redux_store" do @@ -544,52 +438,6 @@ def helper.append_javascript_pack_tag(name, **options) it { expect(expect(store).target).to script_tag_be_included(react_store_script) } - - context "with Pro license warning" do - let(:badge_html_string) { "React On Rails Pro Required" } - - before do - allow(Rails.logger).to receive(:warn) - end - - context "when Pro license is NOT installed and immediate_hydration is true" do - subject(:store) { redux_store("reduxStore", props: props, immediate_hydration: true) } - - before do - allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(false) - end - - it { is_expected.to include(badge_html_string) } - - it "logs a warning" do - store - expect(Rails.logger).to have_received(:warn) - .with(a_string_matching(/The 'immediate_hydration' feature requires/)) - end - end - - context "when Pro license is NOT installed and immediate_hydration is false" do - subject(:store) { redux_store("reduxStore", props: props, immediate_hydration: false) } - - before do - allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(false) - end - - it { is_expected.not_to include(badge_html_string) } - end - - context "when Pro license IS installed and immediate_hydration is true" do - subject(:store) { redux_store("reduxStore", props: props, immediate_hydration: true) } - - before do - allow(ReactOnRails::Utils).to receive_messages( - react_on_rails_pro?: true - ) - end - - it { is_expected.not_to include(badge_html_string) } - end - end end describe "#server_render_js", :js, type: :system do From 117fb1f8230505e35afdf61f9b30918f11244047 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:49:54 +0000 Subject: [PATCH 08/17] Add CHANGELOG entry for Pro warning badge removal Document the removal of the visual warning badge that appeared when non-Pro users attempted to enable Pro-only features. Pro features are now silently disabled when a Pro license is not available. Co-authored-by: Abanoub Ghadban --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c6d318fd4..14ea373a80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,8 @@ Changes since the last non-beta release. - **Shakapacker 9.2.0 Upgrade**: Upgraded Shakapacker from 9.1.0 to 9.2.0. This minor version update adds a new `bin/shakapacker-config` utility for debugging webpack/rspack configurations with doctor mode, save mode, and stdout mode options. Supports YAML, JSON, and Node.js inspect output formats. by [justin808](https://github.com/justin808). +- **Removed Pro Warning Badge**: Removed the visual warning badge that appeared when non-Pro users attempted to enable Pro-only features like `immediate_hydration`. Pro features are now silently disabled when a Pro license is not available, providing a cleaner user experience without intrusive warning banners. [PR 1993](https://github.com/shakacode/react_on_rails/pull/1993) by [AbanoubGhadban](https://github.com/AbanoubGhadban). + #### Bug Fixes - **Use as Git dependency**: All packages can now be installed as Git dependencies. This is useful for development and testing purposes. See [CONTRIBUTING.md](./CONTRIBUTING.md#git-dependencies) for documentation. [PR #1873](https://github.com/shakacode/react_on_rails/pull/1873) by [alexeyr-ci2](https://github.com/alexeyr-ci2). From 81bb42a37d510c8a6c2330058e317a1cb2fed4bf Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 13 Nov 2025 00:41:15 +0200 Subject: [PATCH 09/17] Remove immediate_hydration config - Pro always hydrates immediately (#1997) Why Simplify configuration for pro users. Still allows override at react_component level. Summary Removed visual warning badges for Pro features, made immediate hydration automatic for Pro users, and removed global config option. Key improvements - Automatic immediate hydration for Pro users (no config needed) - Rails logger warnings replace visual badges for better UX - Component-level overrides still supported via helper parameters Impact - Existing: Pro users unchanged; immediate hydration auto-enabled - New: Simpler config; non-Pro get warnings in logs, not UI Risks Breaking: config.immediate_hydration removed from initializers (see CHANGELOG migration steps). Security: None. --- CHANGELOG.md | 14 ++++ lib/react_on_rails/configuration.rb | 7 +- lib/react_on_rails/controller.rb | 6 +- lib/react_on_rails/doctor.rb | 10 +-- lib/react_on_rails/helper.rb | 6 +- lib/react_on_rails/pro_helper.rb | 2 - lib/react_on_rails/pro_utils.rb | 25 ++---- .../react_component/render_options.rb | 17 +++- .../config/initializers/react_on_rails.rb | 1 - .../helpers/react_on_rails_helper_spec.rb | 11 +-- spec/dummy/spec/system/integration_spec.rb | 7 +- .../react_component/render_options_spec.rb | 83 ++++++++++++++++++- 12 files changed, 130 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14ea373a80..0880328d5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,12 +51,26 @@ Changes since the last non-beta release. - **Removed Pro Warning Badge**: Removed the visual warning badge that appeared when non-Pro users attempted to enable Pro-only features like `immediate_hydration`. Pro features are now silently disabled when a Pro license is not available, providing a cleaner user experience without intrusive warning banners. [PR 1993](https://github.com/shakacode/react_on_rails/pull/1993) by [AbanoubGhadban](https://github.com/AbanoubGhadban). +- **`immediate_hydration` now automatically enabled for Pro users**: The `config.immediate_hydration` configuration option has been removed. Immediate hydration is now automatically enabled for React on Rails Pro users and disabled for non-Pro users, simplifying configuration while providing optimal performance by default. Component-level overrides are still supported via the `immediate_hydration` parameter on `react_component`, `redux_store`, and `stream_react_component` helpers. [PR 1997](https://github.com/shakacode/react_on_rails/pull/1997) by [AbanoubGhadban](https://github.com/AbanoubGhadban). + #### Bug Fixes - **Use as Git dependency**: All packages can now be installed as Git dependencies. This is useful for development and testing purposes. See [CONTRIBUTING.md](./CONTRIBUTING.md#git-dependencies) for documentation. [PR #1873](https://github.com/shakacode/react_on_rails/pull/1873) by [alexeyr-ci2](https://github.com/alexeyr-ci2). #### Breaking Changes +- **`config.immediate_hydration` configuration removed**: The `config.immediate_hydration` setting in `config/initializers/react_on_rails.rb` has been removed. Immediate hydration is now automatically enabled for React on Rails Pro users and automatically disabled for non-Pro users. + + **Migration steps:** + + - Remove any `config.immediate_hydration = true` or `config.immediate_hydration = false` lines from your `config/initializers/react_on_rails.rb` file + - Pro users: No action needed - immediate hydration is now enabled automatically for optimal performance + - Non-Pro users: No action needed - standard hydration behavior continues to work as before + - Component-level overrides: You can still override behavior per-component using `react_component("MyComponent", immediate_hydration: false)` or `redux_store("MyStore", immediate_hydration: true)` + - If a non-Pro user explicitly sets `immediate_hydration: true` on a component, a warning will be logged and the component will fall back to standard hydration + + [PR 1997](https://github.com/shakacode/react_on_rails/pull/1997) by [AbanoubGhadban](https://github.com/AbanoubGhadban). + - **React on Rails Core Package**: Several Pro-only methods have been removed from the core package and are now exclusively available in the `react-on-rails-pro` package. If you're using any of the following methods, you'll need to migrate to React on Rails Pro: - `getOrWaitForComponent()` - `getOrWaitForStore()` diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index 6d89cd071e..e3efa2c29c 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -41,8 +41,6 @@ def self.configuration components_subdirectory: nil, make_generated_server_bundle_the_entrypoint: false, defer_generated_component_packs: false, - # 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. @@ -64,7 +62,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, :immediate_hydration, + :generated_component_packs_loading_strategy, :component_registry_timeout, :server_bundle_output_path, :enforce_private_server_bundles @@ -81,7 +79,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, immediate_hydration: nil, + components_subdirectory: nil, auto_load_bundle: nil, component_registry_timeout: nil, server_bundle_output_path: nil, enforce_private_server_bundles: nil) self.node_modules_location = node_modules_location.present? ? node_modules_location : Rails.root self.generated_assets_dirs = generated_assets_dirs @@ -122,7 +120,6 @@ 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.immediate_hydration = immediate_hydration self.generated_component_packs_loading_strategy = generated_component_packs_loading_strategy self.server_bundle_output_path = server_bundle_output_path self.enforce_private_server_bundles = enforce_private_server_bundles diff --git a/lib/react_on_rails/controller.rb b/lib/react_on_rails/controller.rb index ae254b1a5a..364d72e888 100644 --- a/lib/react_on_rails/controller.rb +++ b/lib/react_on_rails/controller.rb @@ -9,13 +9,13 @@ 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. + # immediate_hydration: React on Rails Pro (licensed) feature. When nil (default), Pro users get + # immediate hydration, non-Pro users don't. Can be explicitly overridden. # # 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: {}, immediate_hydration: nil) - immediate_hydration = ReactOnRails.configuration.immediate_hydration if immediate_hydration.nil? + immediate_hydration = ReactOnRails::ProUtils.immediate_hydration_enabled? if immediate_hydration.nil? redux_store_data = { store_name: store_name, props: props, immediate_hydration: immediate_hydration } diff --git a/lib/react_on_rails/doctor.rb b/lib/react_on_rails/doctor.rb index 54ed06dbec..4d12977055 100644 --- a/lib/react_on_rails/doctor.rb +++ b/lib/react_on_rails/doctor.rb @@ -701,7 +701,7 @@ def analyze_server_rendering_config(content) end # rubocop:enable Metrics/AbcSize - # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity + # rubocop:disable Metrics/CyclomaticComplexity def analyze_performance_config(content) checker.add_info("\n⚔ Performance & Loading:") @@ -732,19 +732,13 @@ def analyze_performance_config(content) auto_load_match = content.match(/config\.auto_load_bundle\s*=\s*([^\s\n,]+)/) checker.add_info(" auto_load_bundle: #{auto_load_match[1]}") if auto_load_match - # Immediate hydration (Pro feature) - immediate_hydration_match = content.match(/config\.immediate_hydration\s*=\s*([^\s\n,]+)/) - if immediate_hydration_match - checker.add_info(" immediate_hydration: #{immediate_hydration_match[1]} (React on Rails Pro)") - end - # Component registry timeout timeout_match = content.match(/config\.component_registry_timeout\s*=\s*([^\s\n,]+)/) return unless timeout_match checker.add_info(" component_registry_timeout: #{timeout_match[1]}ms") end - # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity + # rubocop:enable Metrics/CyclomaticComplexity # rubocop:disable Metrics/AbcSize def analyze_development_config(content) diff --git a/lib/react_on_rails/helper.rb b/lib/react_on_rails/helper.rb index 92f4e0c375..8b8b487653 100644 --- a/lib/react_on_rails/helper.rb +++ b/lib/react_on_rails/helper.rb @@ -155,10 +155,10 @@ 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. - # 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. + # immediate_hydration: nil -- React on Rails Pro (licensed) feature. When nil (default), Pro users + # get immediate hydration, non-Pro users don't. Can be explicitly overridden. def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil) - immediate_hydration = ReactOnRails.configuration.immediate_hydration if immediate_hydration.nil? + immediate_hydration = ReactOnRails::ProUtils.immediate_hydration_enabled? if immediate_hydration.nil? redux_store_data = { store_name: store_name, props: props, diff --git a/lib/react_on_rails/pro_helper.rb b/lib/react_on_rails/pro_helper.rb index 3a8b58b3ee..4f3c020d77 100644 --- a/lib/react_on_rails/pro_helper.rb +++ b/lib/react_on_rails/pro_helper.rb @@ -38,8 +38,6 @@ def generate_component_script(render_options) # Generates the complete store hydration script tag. # Handles both immediate hydration (Pro feature) and standard cases. def generate_store_script(redux_store_data) - redux_store_data = ReactOnRails::ProUtils.disable_pro_render_options_if_not_licensed(redux_store_data) - store_hydration_data = content_tag(:script, json_safe_and_pretty(redux_store_data[:props]).html_safe, type: "application/json", diff --git a/lib/react_on_rails/pro_utils.rb b/lib/react_on_rails/pro_utils.rb index bb222630cd..7d239faf8b 100644 --- a/lib/react_on_rails/pro_utils.rb +++ b/lib/react_on_rails/pro_utils.rb @@ -2,32 +2,17 @@ module ReactOnRails module ProUtils - PRO_ONLY_OPTIONS = %i[immediate_hydration].freeze - # Checks if React on Rails Pro features are available # @return [Boolean] true if Pro is installed and licensed, false otherwise def self.support_pro_features? ReactOnRails::Utils.react_on_rails_pro? end - def self.disable_pro_render_options_if_not_licensed(raw_options) - return raw_options if support_pro_features? - - raw_options_after_disable = raw_options.dup - - PRO_ONLY_OPTIONS.each do |option| - # Determine if this option is enabled (either explicitly or via global config) - option_enabled = if raw_options[option].nil? - ReactOnRails.configuration.send(option) - else - raw_options[option] - end - - # Silently disable the option if it's enabled but Pro is not available - raw_options_after_disable[option] = false if option_enabled - end - - raw_options_after_disable + # Returns whether immediate hydration should be enabled + # Pro users always get immediate hydration, non-Pro users never do + # @return [Boolean] true if Pro is available, false otherwise + def self.immediate_hydration_enabled? + support_pro_features? end end end diff --git a/lib/react_on_rails/react_component/render_options.rb b/lib/react_on_rails/react_component/render_options.rb index b6bc5f5fe3..617286405b 100644 --- a/lib/react_on_rails/react_component/render_options.rb +++ b/lib/react_on_rails/react_component/render_options.rb @@ -15,7 +15,7 @@ class RenderOptions # TODO: remove the required for named params def initialize(react_component_name: required("react_component_name"), options: required("options")) @react_component_name = react_component_name.camelize - @options = ReactOnRails::ProUtils.disable_pro_render_options_if_not_licensed(options) + @options = options end attr_reader :react_component_name @@ -97,7 +97,20 @@ def logging_on_server end def immediate_hydration - retrieve_configuration_value_for(:immediate_hydration) + explicit_value = options[:immediate_hydration] + + # Warn if non-Pro user explicitly sets immediate_hydration: true + if explicit_value == true && !ReactOnRails::Utils.react_on_rails_pro? + Rails.logger.warn <<~WARNING + [REACT ON RAILS] Warning: immediate_hydration: true requires a React on Rails Pro license. + Component '#{react_component_name}' will fall back to standard hydration behavior. + Visit https://www.shakacode.com/react-on-rails-pro/ for licensing information. + WARNING + end + + options.fetch(:immediate_hydration) do + ReactOnRails::ProUtils.immediate_hydration_enabled? + end end def to_s diff --git a/spec/dummy/config/initializers/react_on_rails.rb b/spec/dummy/config/initializers/react_on_rails.rb index 54c2f40d5c..83598f028a 100644 --- a/spec/dummy/config/initializers/react_on_rails.rb +++ b/spec/dummy/config/initializers/react_on_rails.rb @@ -41,6 +41,5 @@ def self.adjust_props_for_client_side_hydration(component_name, props) config.rendering_props_extension = RenderingPropsExtension config.components_subdirectory = "startup" config.auto_load_bundle = true - config.immediate_hydration = false config.generated_component_packs_loading_strategy = :defer end diff --git a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb index 3b36f0e362..ffd800f203 100644 --- a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb +++ b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb @@ -38,15 +38,8 @@ def self.pro_attribution_comment stub_const("ReactOnRailsPro", pro_module) stub_const("ReactOnRailsPro::Utils", utils_module) - # Configure immediate_hydration to true for tests since they expect that behavior - ReactOnRails.configure do |config| - config.immediate_hydration = true - end - end - - after do - # Reset to default - avoid validation issues by setting directly - ReactOnRails.configuration.immediate_hydration = false + # Stub immediate_hydration_enabled? to return true for tests since they expect that behavior + allow(ReactOnRails::ProUtils).to receive(:immediate_hydration_enabled?).and_return(true) end let(:hash) do diff --git a/spec/dummy/spec/system/integration_spec.rb b/spec/dummy/spec/system/integration_spec.rb index 57212d1ca0..dac027f8af 100644 --- a/spec/dummy/spec/system/integration_spec.rb +++ b/spec/dummy/spec/system/integration_spec.rb @@ -104,12 +104,9 @@ def self.pro_attribution_comment end stub_const("ReactOnRailsPro", pro_module) stub_const("ReactOnRailsPro::Utils", utils_module) - end - around do |example| - ReactOnRails.configure { |config| config.immediate_hydration = true } - example.run - ReactOnRails.configure { |config| config.immediate_hydration = false } + # Stub immediate_hydration_enabled? to return true for tests since they expect that behavior + allow(ReactOnRails::ProUtils).to receive(:immediate_hydration_enabled?).and_return(true) end end diff --git a/spec/react_on_rails/react_component/render_options_spec.rb b/spec/react_on_rails/react_component/render_options_spec.rb index 8ef62cba5c..8ff7e5569e 100644 --- a/spec/react_on_rails/react_component/render_options_spec.rb +++ b/spec/react_on_rails/react_component/render_options_spec.rb @@ -9,7 +9,6 @@ replay_console raise_on_prerender_error random_dom_id - immediate_hydration ].freeze def the_attrs(react_component_name: "App", options: {}) @@ -164,4 +163,86 @@ def the_attrs(react_component_name: "App", options: {}) end end end + + describe "#immediate_hydration" do + context "with Pro license" do + before do + allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(true) + end + + context "with immediate_hydration option set to true" do + it "returns true" do + options = { immediate_hydration: true } + attrs = the_attrs(options: options) + + opts = described_class.new(**attrs) + + expect(opts.immediate_hydration).to be true + end + end + + context "with immediate_hydration option set to false" do + it "returns false (override)" do + options = { immediate_hydration: false } + attrs = the_attrs(options: options) + + opts = described_class.new(**attrs) + + expect(opts.immediate_hydration).to be false + end + end + + context "without immediate_hydration option" do + it "returns true (Pro default)" do + allow(ReactOnRails::ProUtils).to receive(:immediate_hydration_enabled?).and_return(true) + attrs = the_attrs + + opts = described_class.new(**attrs) + + expect(opts.immediate_hydration).to be true + end + end + end + + context "without Pro license" do + before do + allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(false) + end + + context "with immediate_hydration option set to true (not recommended)" do + it "returns true but logs a warning" do + options = { immediate_hydration: true } + attrs = the_attrs(options: options) + + expect(Rails.logger).to receive(:warn).with(/immediate_hydration: true requires a React on Rails Pro license/) + + opts = described_class.new(**attrs) + + expect(opts.immediate_hydration).to be true + end + end + + context "with immediate_hydration option set to false" do + it "returns false" do + options = { immediate_hydration: false } + attrs = the_attrs(options: options) + + opts = described_class.new(**attrs) + + expect(opts.immediate_hydration).to be false + end + end + + context "without immediate_hydration option" do + it "returns false (non-Pro default)" do + allow(ReactOnRails::ProUtils).to receive(:immediate_hydration_enabled?).and_return(false) + attrs = the_attrs + + opts = described_class.new(**attrs) + + expect(opts.immediate_hydration).to be false + end + end + end + end end From 4df0fc91b73c5aaf95e554f51bd36a1de1345850 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Wed, 12 Nov 2025 13:01:33 -1000 Subject: [PATCH 10/17] Inline immediate_hydration_enabled? method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the intermediate immediate_hydration_enabled? helper method from ProUtils and directly use ReactOnRails::Utils.react_on_rails_pro? instead. This simplifies the code by removing an unnecessary layer of indirection. The immediate_hydration_enabled? method was just a wrapper that called support_pro_features?, which itself just calls react_on_rails_pro?. Changes: - Remove immediate_hydration_enabled? method from ProUtils - Update all callers to use ReactOnRails::Utils.react_on_rails_pro? directly: - render_options.rb: immediate_hydration method - helper.rb: redux_store method - controller.rb: redux_store method - Update test mocks to stub react_on_rails_pro? instead of immediate_hydration_enabled? - Remove unnecessary test stubs in render_options_spec.rb šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/react_on_rails/controller.rb | 2 +- lib/react_on_rails/helper.rb | 2 +- lib/react_on_rails/pro_utils.rb | 7 ------- lib/react_on_rails/react_component/render_options.rb | 2 +- spec/dummy/spec/helpers/react_on_rails_helper_spec.rb | 4 ++-- spec/dummy/spec/system/integration_spec.rb | 4 ++-- spec/react_on_rails/react_component/render_options_spec.rb | 2 -- 7 files changed, 7 insertions(+), 16 deletions(-) diff --git a/lib/react_on_rails/controller.rb b/lib/react_on_rails/controller.rb index 364d72e888..479704ec80 100644 --- a/lib/react_on_rails/controller.rb +++ b/lib/react_on_rails/controller.rb @@ -15,7 +15,7 @@ module Controller # 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: {}, immediate_hydration: nil) - immediate_hydration = ReactOnRails::ProUtils.immediate_hydration_enabled? if immediate_hydration.nil? + immediate_hydration = ReactOnRails::Utils.react_on_rails_pro? if immediate_hydration.nil? redux_store_data = { store_name: store_name, props: props, immediate_hydration: immediate_hydration } diff --git a/lib/react_on_rails/helper.rb b/lib/react_on_rails/helper.rb index 8b8b487653..0cde2bb6ef 100644 --- a/lib/react_on_rails/helper.rb +++ b/lib/react_on_rails/helper.rb @@ -158,7 +158,7 @@ def react_component_hash(component_name, options = {}) # immediate_hydration: nil -- React on Rails Pro (licensed) feature. When nil (default), Pro users # get immediate hydration, non-Pro users don't. Can be explicitly overridden. def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil) - immediate_hydration = ReactOnRails::ProUtils.immediate_hydration_enabled? if immediate_hydration.nil? + immediate_hydration = ReactOnRails::Utils.react_on_rails_pro? if immediate_hydration.nil? redux_store_data = { store_name: store_name, props: props, diff --git a/lib/react_on_rails/pro_utils.rb b/lib/react_on_rails/pro_utils.rb index 7d239faf8b..8eada9d67a 100644 --- a/lib/react_on_rails/pro_utils.rb +++ b/lib/react_on_rails/pro_utils.rb @@ -7,12 +7,5 @@ module ProUtils def self.support_pro_features? ReactOnRails::Utils.react_on_rails_pro? end - - # Returns whether immediate hydration should be enabled - # Pro users always get immediate hydration, non-Pro users never do - # @return [Boolean] true if Pro is available, false otherwise - def self.immediate_hydration_enabled? - support_pro_features? - end end end diff --git a/lib/react_on_rails/react_component/render_options.rb b/lib/react_on_rails/react_component/render_options.rb index 617286405b..9faf91ee5d 100644 --- a/lib/react_on_rails/react_component/render_options.rb +++ b/lib/react_on_rails/react_component/render_options.rb @@ -109,7 +109,7 @@ def immediate_hydration end options.fetch(:immediate_hydration) do - ReactOnRails::ProUtils.immediate_hydration_enabled? + ReactOnRails::Utils.react_on_rails_pro? end end diff --git a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb index ffd800f203..62eb531e81 100644 --- a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb +++ b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb @@ -38,8 +38,8 @@ def self.pro_attribution_comment stub_const("ReactOnRailsPro", pro_module) stub_const("ReactOnRailsPro::Utils", utils_module) - # Stub immediate_hydration_enabled? to return true for tests since they expect that behavior - allow(ReactOnRails::ProUtils).to receive(:immediate_hydration_enabled?).and_return(true) + # Stub react_on_rails_pro? to return true for tests since they expect that behavior + allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(true) end let(:hash) do diff --git a/spec/dummy/spec/system/integration_spec.rb b/spec/dummy/spec/system/integration_spec.rb index dac027f8af..452606b289 100644 --- a/spec/dummy/spec/system/integration_spec.rb +++ b/spec/dummy/spec/system/integration_spec.rb @@ -105,8 +105,8 @@ def self.pro_attribution_comment stub_const("ReactOnRailsPro", pro_module) stub_const("ReactOnRailsPro::Utils", utils_module) - # Stub immediate_hydration_enabled? to return true for tests since they expect that behavior - allow(ReactOnRails::ProUtils).to receive(:immediate_hydration_enabled?).and_return(true) + # Stub react_on_rails_pro? to return true for tests since they expect that behavior + allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(true) end end diff --git a/spec/react_on_rails/react_component/render_options_spec.rb b/spec/react_on_rails/react_component/render_options_spec.rb index 8ff7e5569e..69a00c1015 100644 --- a/spec/react_on_rails/react_component/render_options_spec.rb +++ b/spec/react_on_rails/react_component/render_options_spec.rb @@ -194,7 +194,6 @@ def the_attrs(react_component_name: "App", options: {}) context "without immediate_hydration option" do it "returns true (Pro default)" do - allow(ReactOnRails::ProUtils).to receive(:immediate_hydration_enabled?).and_return(true) attrs = the_attrs opts = described_class.new(**attrs) @@ -235,7 +234,6 @@ def the_attrs(react_component_name: "App", options: {}) context "without immediate_hydration option" do it "returns false (non-Pro default)" do - allow(ReactOnRails::ProUtils).to receive(:immediate_hydration_enabled?).and_return(false) attrs = the_attrs opts = described_class.new(**attrs) From 3d68c4ced7f869b9452b3ccf4ff2fec82b0fec77 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Wed, 12 Nov 2025 14:32:35 -1000 Subject: [PATCH 11/17] Remove unused ProUtils module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After inlining immediate_hydration_enabled?, the ProUtils module only contains a single method (support_pro_features?) that is not used anywhere in the codebase. This commit removes the entire module as dead code. Changes: - Delete lib/react_on_rails/pro_utils.rb - Remove require "react_on_rails/pro_utils" from render_options.rb This simplifies the codebase by removing an unnecessary abstraction layer. šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/react_on_rails/pro_utils.rb | 11 ----------- lib/react_on_rails/react_component/render_options.rb | 1 - 2 files changed, 12 deletions(-) delete mode 100644 lib/react_on_rails/pro_utils.rb diff --git a/lib/react_on_rails/pro_utils.rb b/lib/react_on_rails/pro_utils.rb deleted file mode 100644 index 8eada9d67a..0000000000 --- a/lib/react_on_rails/pro_utils.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module ReactOnRails - module ProUtils - # Checks if React on Rails Pro features are available - # @return [Boolean] true if Pro is installed and licensed, false otherwise - def self.support_pro_features? - ReactOnRails::Utils.react_on_rails_pro? - end - end -end diff --git a/lib/react_on_rails/react_component/render_options.rb b/lib/react_on_rails/react_component/render_options.rb index 9faf91ee5d..d0186c264b 100644 --- a/lib/react_on_rails/react_component/render_options.rb +++ b/lib/react_on_rails/react_component/render_options.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "react_on_rails/utils" -require "react_on_rails/pro_utils" module ReactOnRails module ReactComponent From 5b0f1f7f4ca4b7b9f87d5e87113c52d857572c1d Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Wed, 12 Nov 2025 15:02:16 -1000 Subject: [PATCH 12/17] Address review comments: Enforce immediate_hydration fallback for non-Pro users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit addresses several issues identified in code review: 1. Fixed immediate_hydration logic in RenderOptions - Non-Pro users setting immediate_hydration: true now get false enforced - Previously returned true despite warning about fallback - Added explicit return false to match warning message behavior 2. Added warning logic to redux_store methods - Both helper.rb and controller.rb now warn non-Pro users - Enforces fallback by overriding to false when Pro license missing - Provides consistent behavior with react_component helper 3. Updated test expectations - render_options_spec.rb now expects false (not true) for non-Pro users - Added comprehensive redux_store tests for warning behavior - Tests cover all three scenarios: true, false, and nil values 4. Clarified CHANGELOG.md - Explicitly states fallback is enforced (value overridden to false) - Covers both components and stores in the migration notes All tests passing and RuboCop clean. šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CHANGELOG.md | 2 +- lib/react_on_rails/controller.rb | 13 ++++++- lib/react_on_rails/helper.rb | 12 +++++- .../react_component/render_options.rb | 3 +- .../helpers/react_on_rails_helper_spec.rb | 37 +++++++++++++++++++ .../react_component/render_options_spec.rb | 4 +- 6 files changed, 65 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0880328d5e..857d3a2dbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,7 +67,7 @@ Changes since the last non-beta release. - Pro users: No action needed - immediate hydration is now enabled automatically for optimal performance - Non-Pro users: No action needed - standard hydration behavior continues to work as before - Component-level overrides: You can still override behavior per-component using `react_component("MyComponent", immediate_hydration: false)` or `redux_store("MyStore", immediate_hydration: true)` - - If a non-Pro user explicitly sets `immediate_hydration: true` on a component, a warning will be logged and the component will fall back to standard hydration + - If a non-Pro user explicitly sets `immediate_hydration: true` on a component or store, a warning will be logged and it will be enforced to fall back to standard hydration (the value will be overridden to `false`) [PR 1997](https://github.com/shakacode/react_on_rails/pull/1997) by [AbanoubGhadban](https://github.com/AbanoubGhadban). diff --git a/lib/react_on_rails/controller.rb b/lib/react_on_rails/controller.rb index 479704ec80..24b1b45ecc 100644 --- a/lib/react_on_rails/controller.rb +++ b/lib/react_on_rails/controller.rb @@ -15,7 +15,18 @@ module Controller # 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: {}, immediate_hydration: nil) - immediate_hydration = ReactOnRails::Utils.react_on_rails_pro? if immediate_hydration.nil? + # If non-Pro user explicitly sets immediate_hydration: true, warn and override to false + if immediate_hydration == true && !ReactOnRails::Utils.react_on_rails_pro? + Rails.logger.warn <<~WARNING + [REACT ON RAILS] Warning: immediate_hydration: true requires a React on Rails Pro license. + Store '#{store_name}' will fall back to standard hydration behavior. + Visit https://www.shakacode.com/react-on-rails-pro/ for licensing information. + WARNING + immediate_hydration = false + elsif immediate_hydration.nil? + immediate_hydration = ReactOnRails::Utils.react_on_rails_pro? + end + redux_store_data = { store_name: store_name, props: props, immediate_hydration: immediate_hydration } diff --git a/lib/react_on_rails/helper.rb b/lib/react_on_rails/helper.rb index 0cde2bb6ef..e578ea2415 100644 --- a/lib/react_on_rails/helper.rb +++ b/lib/react_on_rails/helper.rb @@ -158,7 +158,17 @@ def react_component_hash(component_name, options = {}) # immediate_hydration: nil -- React on Rails Pro (licensed) feature. When nil (default), Pro users # get immediate hydration, non-Pro users don't. Can be explicitly overridden. def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil) - immediate_hydration = ReactOnRails::Utils.react_on_rails_pro? if immediate_hydration.nil? + # If non-Pro user explicitly sets immediate_hydration: true, warn and override to false + if immediate_hydration == true && !ReactOnRails::Utils.react_on_rails_pro? + Rails.logger.warn <<~WARNING + [REACT ON RAILS] Warning: immediate_hydration: true requires a React on Rails Pro license. + Store '#{store_name}' will fall back to standard hydration behavior. + Visit https://www.shakacode.com/react-on-rails-pro/ for licensing information. + WARNING + immediate_hydration = false + elsif immediate_hydration.nil? + immediate_hydration = ReactOnRails::Utils.react_on_rails_pro? + end redux_store_data = { store_name: store_name, props: props, diff --git a/lib/react_on_rails/react_component/render_options.rb b/lib/react_on_rails/react_component/render_options.rb index d0186c264b..5549028868 100644 --- a/lib/react_on_rails/react_component/render_options.rb +++ b/lib/react_on_rails/react_component/render_options.rb @@ -98,13 +98,14 @@ def logging_on_server def immediate_hydration explicit_value = options[:immediate_hydration] - # Warn if non-Pro user explicitly sets immediate_hydration: true + # If non-Pro user explicitly sets immediate_hydration: true, warn and override to false if explicit_value == true && !ReactOnRails::Utils.react_on_rails_pro? Rails.logger.warn <<~WARNING [REACT ON RAILS] Warning: immediate_hydration: true requires a React on Rails Pro license. Component '#{react_component_name}' will fall back to standard hydration behavior. Visit https://www.shakacode.com/react-on-rails-pro/ for licensing information. WARNING + return false # Force fallback to standard hydration end options.fetch(:immediate_hydration) do diff --git a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb index 62eb531e81..a13c61017a 100644 --- a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb +++ b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb @@ -431,6 +431,43 @@ def helper.append_javascript_pack_tag(name, **options) it { expect(expect(store).target).to script_tag_be_included(react_store_script) } + + context "without Pro license" do + before do + allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(false) + end + + context "with immediate_hydration option set to true (not recommended)" do + it "returns false for immediate_hydration and logs a warning" do + expect(Rails.logger).to receive(:warn).with(/immediate_hydration: true requires a React on Rails Pro license/) + + result = redux_store("reduxStore", props: props, immediate_hydration: true) + + # Verify that the store tag does NOT have immediate hydration enabled + expect(result).not_to include('data-immediate-hydration="true"') + end + end + + context "with immediate_hydration option set to false" do + it "returns false for immediate_hydration without warning" do + expect(Rails.logger).not_to receive(:warn) + + result = redux_store("reduxStore", props: props, immediate_hydration: false) + + # Verify that the store tag does NOT have immediate hydration enabled + expect(result).not_to include('data-immediate-hydration="true"') + end + end + + context "without immediate_hydration option (nil)" do + it "defaults to false for non-Pro users" do + result = redux_store("reduxStore", props: props) + + # Verify that the store tag does NOT have immediate hydration enabled + expect(result).not_to include('data-immediate-hydration="true"') + end + end + end end describe "#server_render_js", :js, type: :system do diff --git a/spec/react_on_rails/react_component/render_options_spec.rb b/spec/react_on_rails/react_component/render_options_spec.rb index 69a00c1015..59c494b004 100644 --- a/spec/react_on_rails/react_component/render_options_spec.rb +++ b/spec/react_on_rails/react_component/render_options_spec.rb @@ -209,7 +209,7 @@ def the_attrs(react_component_name: "App", options: {}) end context "with immediate_hydration option set to true (not recommended)" do - it "returns true but logs a warning" do + it "returns false and logs a warning (enforces fallback)" do options = { immediate_hydration: true } attrs = the_attrs(options: options) @@ -217,7 +217,7 @@ def the_attrs(react_component_name: "App", options: {}) opts = described_class.new(**attrs) - expect(opts.immediate_hydration).to be true + expect(opts.immediate_hydration).to be false end end From 6a19cde527ff6b66dc31d98efc3b0c034e84bb37 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Wed, 12 Nov 2025 15:43:01 -1000 Subject: [PATCH 13/17] Refactor immediate_hydration handling and add deprecation warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address code review feedback: 1. Extract duplicated warning message to shared utility method - Add ReactOnRails::Utils.immediate_hydration_pro_license_warning helper - Replace inline warning messages in controller, helper, and render_options 2. Add deprecation warning for config.immediate_hydration - Add getter/setter methods that log deprecation warnings - Direct users to remove the deprecated config from initializers - Maintains backward compatibility while guiding migration 3. Fix inconsistent nil handling in render_options.rb - Simplify immediate_hydration method logic - Use consistent pattern: explicit value first, then default to Pro status - Remove confusing mix of hash access methods All changes maintain backward compatibility while improving code maintainability and providing better user guidance. šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/react_on_rails/configuration.rb | 20 +++++++++++++++++++ lib/react_on_rails/controller.rb | 6 +----- lib/react_on_rails/helper.rb | 6 +----- .../react_component/render_options.rb | 14 ++++++------- lib/react_on_rails/utils.rb | 6 ++++++ 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index e3efa2c29c..2fb826f5f5 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -66,6 +66,26 @@ class Configuration :component_registry_timeout, :server_bundle_output_path, :enforce_private_server_bundles + # Deprecated: immediate_hydration configuration has been removed + def immediate_hydration=(value) + Rails.logger.warn <<~WARNING + [REACT ON RAILS] The 'config.immediate_hydration' configuration option is deprecated and no longer used. + Immediate hydration is now automatically enabled for React on Rails Pro users. + Please remove 'config.immediate_hydration = #{value}' from your config/initializers/react_on_rails.rb file. + See CHANGELOG.md for migration instructions. + WARNING + end + + def immediate_hydration + Rails.logger.warn <<~WARNING + [REACT ON RAILS] The 'config.immediate_hydration' configuration option is deprecated and no longer used. + Immediate hydration is now automatically enabled for React on Rails Pro users. + Please remove any references to 'config.immediate_hydration' from your config/initializers/react_on_rails.rb file. + See CHANGELOG.md for migration instructions. + WARNING + nil + end + # rubocop:disable Metrics/AbcSize def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender: nil, replay_console: nil, make_generated_server_bundle_the_entrypoint: nil, diff --git a/lib/react_on_rails/controller.rb b/lib/react_on_rails/controller.rb index 24b1b45ecc..b34821536a 100644 --- a/lib/react_on_rails/controller.rb +++ b/lib/react_on_rails/controller.rb @@ -17,11 +17,7 @@ module Controller def redux_store(store_name, props: {}, immediate_hydration: nil) # If non-Pro user explicitly sets immediate_hydration: true, warn and override to false if immediate_hydration == true && !ReactOnRails::Utils.react_on_rails_pro? - Rails.logger.warn <<~WARNING - [REACT ON RAILS] Warning: immediate_hydration: true requires a React on Rails Pro license. - Store '#{store_name}' will fall back to standard hydration behavior. - Visit https://www.shakacode.com/react-on-rails-pro/ for licensing information. - WARNING + Rails.logger.warn ReactOnRails::Utils.immediate_hydration_pro_license_warning(store_name, "Store") immediate_hydration = false elsif immediate_hydration.nil? immediate_hydration = ReactOnRails::Utils.react_on_rails_pro? diff --git a/lib/react_on_rails/helper.rb b/lib/react_on_rails/helper.rb index e578ea2415..ce7b5173bf 100644 --- a/lib/react_on_rails/helper.rb +++ b/lib/react_on_rails/helper.rb @@ -160,11 +160,7 @@ def react_component_hash(component_name, options = {}) def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil) # If non-Pro user explicitly sets immediate_hydration: true, warn and override to false if immediate_hydration == true && !ReactOnRails::Utils.react_on_rails_pro? - Rails.logger.warn <<~WARNING - [REACT ON RAILS] Warning: immediate_hydration: true requires a React on Rails Pro license. - Store '#{store_name}' will fall back to standard hydration behavior. - Visit https://www.shakacode.com/react-on-rails-pro/ for licensing information. - WARNING + Rails.logger.warn ReactOnRails::Utils.immediate_hydration_pro_license_warning(store_name, "Store") immediate_hydration = false elsif immediate_hydration.nil? immediate_hydration = ReactOnRails::Utils.react_on_rails_pro? diff --git a/lib/react_on_rails/react_component/render_options.rb b/lib/react_on_rails/react_component/render_options.rb index 5549028868..1b97cee702 100644 --- a/lib/react_on_rails/react_component/render_options.rb +++ b/lib/react_on_rails/react_component/render_options.rb @@ -100,17 +100,15 @@ def immediate_hydration # If non-Pro user explicitly sets immediate_hydration: true, warn and override to false if explicit_value == true && !ReactOnRails::Utils.react_on_rails_pro? - Rails.logger.warn <<~WARNING - [REACT ON RAILS] Warning: immediate_hydration: true requires a React on Rails Pro license. - Component '#{react_component_name}' will fall back to standard hydration behavior. - Visit https://www.shakacode.com/react-on-rails-pro/ for licensing information. - WARNING + warning = ReactOnRails::Utils.immediate_hydration_pro_license_warning(react_component_name, "Component") + Rails.logger.warn warning return false # Force fallback to standard hydration end - options.fetch(:immediate_hydration) do - ReactOnRails::Utils.react_on_rails_pro? - end + # Return explicit value if provided, otherwise default based on Pro license + return explicit_value unless explicit_value.nil? + + ReactOnRails::Utils.react_on_rails_pro? end def to_s diff --git a/lib/react_on_rails/utils.rb b/lib/react_on_rails/utils.rb index fa96692f86..252c695360 100644 --- a/lib/react_on_rails/utils.rb +++ b/lib/react_on_rails/utils.rb @@ -14,6 +14,12 @@ module Utils Rainbow('To see the full output, set FULL_TEXT_ERRORS=true.').red } ...\n".freeze + def self.immediate_hydration_pro_license_warning(name, type = "Component") + "[REACT ON RAILS] Warning: immediate_hydration: true requires a React on Rails Pro license.\n" \ + "#{type} '#{name}' will fall back to standard hydration behavior.\n" \ + "Visit https://www.shakacode.com/react-on-rails-pro/ for licensing information." + end + # https://forum.shakacode.com/t/yak-of-the-week-ruby-2-4-pathname-empty-changed-to-look-at-file-size/901 # return object if truthy, else return nil def self.truthy_presence(obj) From ce7d8f2ba52239cf4bac04a1ae83f3ad4a58d3e5 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Wed, 12 Nov 2025 15:50:14 -1000 Subject: [PATCH 14/17] Refactor immediate_hydration logic into shared utility method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address code review feedback: 1. Consolidate duplicated immediate_hydration logic - Create ReactOnRails::Utils.normalize_immediate_hydration method - Replace inline logic in controller, helper, and render_options - Reduces code duplication from 3 locations to 1 shared method 2. Document the == true check rationale - Added comprehensive YARD documentation - Explains strict equality is intentional (not truthy check) - Prevents false positives from strings like "yes"/"no" - Type safety handled at call site by Ruby's type system Benefits: - Single source of truth for immediate_hydration normalization - Easier to test and maintain - Clear documentation of behavior - Consistent warning messages across all entry points šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/react_on_rails/controller.rb | 8 +---- lib/react_on_rails/helper.rb | 8 +---- .../react_component/render_options.rb | 18 ++++------- lib/react_on_rails/utils.rb | 30 +++++++++++++++++++ 4 files changed, 37 insertions(+), 27 deletions(-) diff --git a/lib/react_on_rails/controller.rb b/lib/react_on_rails/controller.rb index b34821536a..62736558d3 100644 --- a/lib/react_on_rails/controller.rb +++ b/lib/react_on_rails/controller.rb @@ -15,13 +15,7 @@ module Controller # 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: {}, immediate_hydration: nil) - # If non-Pro user explicitly sets immediate_hydration: true, warn and override to false - if immediate_hydration == true && !ReactOnRails::Utils.react_on_rails_pro? - Rails.logger.warn ReactOnRails::Utils.immediate_hydration_pro_license_warning(store_name, "Store") - immediate_hydration = false - elsif immediate_hydration.nil? - immediate_hydration = ReactOnRails::Utils.react_on_rails_pro? - end + immediate_hydration = ReactOnRails::Utils.normalize_immediate_hydration(immediate_hydration, store_name, "Store") redux_store_data = { store_name: store_name, props: props, diff --git a/lib/react_on_rails/helper.rb b/lib/react_on_rails/helper.rb index ce7b5173bf..6ef91a2f98 100644 --- a/lib/react_on_rails/helper.rb +++ b/lib/react_on_rails/helper.rb @@ -158,13 +158,7 @@ def react_component_hash(component_name, options = {}) # immediate_hydration: nil -- React on Rails Pro (licensed) feature. When nil (default), Pro users # get immediate hydration, non-Pro users don't. Can be explicitly overridden. def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil) - # If non-Pro user explicitly sets immediate_hydration: true, warn and override to false - if immediate_hydration == true && !ReactOnRails::Utils.react_on_rails_pro? - Rails.logger.warn ReactOnRails::Utils.immediate_hydration_pro_license_warning(store_name, "Store") - immediate_hydration = false - elsif immediate_hydration.nil? - immediate_hydration = ReactOnRails::Utils.react_on_rails_pro? - end + immediate_hydration = ReactOnRails::Utils.normalize_immediate_hydration(immediate_hydration, store_name, "Store") redux_store_data = { store_name: store_name, props: props, diff --git a/lib/react_on_rails/react_component/render_options.rb b/lib/react_on_rails/react_component/render_options.rb index 1b97cee702..deffd7fb61 100644 --- a/lib/react_on_rails/react_component/render_options.rb +++ b/lib/react_on_rails/react_component/render_options.rb @@ -96,19 +96,11 @@ def logging_on_server end def immediate_hydration - explicit_value = options[:immediate_hydration] - - # If non-Pro user explicitly sets immediate_hydration: true, warn and override to false - if explicit_value == true && !ReactOnRails::Utils.react_on_rails_pro? - warning = ReactOnRails::Utils.immediate_hydration_pro_license_warning(react_component_name, "Component") - Rails.logger.warn warning - return false # Force fallback to standard hydration - end - - # Return explicit value if provided, otherwise default based on Pro license - return explicit_value unless explicit_value.nil? - - ReactOnRails::Utils.react_on_rails_pro? + ReactOnRails::Utils.normalize_immediate_hydration( + options[:immediate_hydration], + react_component_name, + "Component" + ) end def to_s diff --git a/lib/react_on_rails/utils.rb b/lib/react_on_rails/utils.rb index 252c695360..3d3177c964 100644 --- a/lib/react_on_rails/utils.rb +++ b/lib/react_on_rails/utils.rb @@ -20,6 +20,36 @@ def self.immediate_hydration_pro_license_warning(name, type = "Component") "Visit https://www.shakacode.com/react-on-rails-pro/ for licensing information." end + # Normalizes the immediate_hydration option value, enforcing Pro license requirements. + # Returns the normalized boolean value for immediate_hydration. + # + # @param value [Boolean, nil] The immediate_hydration option value + # @param name [String] The name of the component/store (for warning messages) + # @param type [String] The type ("Component" or "Store") for warning messages + # @return [Boolean] The normalized immediate_hydration value + # + # Logic: + # - If value is explicitly true (boolean) and no Pro license: warn and return false + # - If value is nil: return true for Pro users, false for non-Pro users + # - Otherwise: return the value as-is (allows explicit false to work) + # + # Note: We check for `== true` (not truthy) to only trigger on explicit boolean true, + # not on strings like "yes" or other truthy values which should be rejected by Ruby's + # type system at the call site. + def self.normalize_immediate_hydration(value, name, type = "Component") + # Strict equality check: only trigger warning for explicit boolean true + if value == true && !react_on_rails_pro? + Rails.logger.warn immediate_hydration_pro_license_warning(name, type) + return false + end + + # If nil, default based on Pro license status + return react_on_rails_pro? if value.nil? + + # Return explicit value (including false) + value + end + # https://forum.shakacode.com/t/yak-of-the-week-ruby-2-4-pathname-empty-changed-to-look-at-file-size/901 # return object if truthy, else return nil def self.truthy_presence(obj) From 4c845d970f1a41de3163d92977bddc1085fa673d Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Wed, 12 Nov 2025 16:13:05 -1000 Subject: [PATCH 15/17] Address code review feedback: improve deprecation warnings and add type validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit addresses three issues identified during code review: 1. Deprecation warnings now use warning-once pattern - Added class instance variable to track if warning shown - Prevents log flooding when config methods accessed repeatedly - Uses recommended Ruby pattern instead of class variables 2. Added type validation for immediate_hydration parameter - Validates that value is true, false, or nil - Raises ArgumentError with clear message for invalid types - Prevents unexpected behavior from truthy non-boolean values 3. Fixed CHANGELOG formatting for consistency - Changed [PR 1993] to [PR #1993] to match other entries All changes include comprehensive test coverage and pass RuboCop. šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CHANGELOG.md | 2 +- lib/react_on_rails/configuration.rb | 13 +++++++++++++ lib/react_on_rails/utils.rb | 12 ++++++++---- .../react_component/render_options_spec.rb | 14 ++++++++++++++ 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 857d3a2dbb..d94dd6c84c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,7 +49,7 @@ Changes since the last non-beta release. - **Shakapacker 9.2.0 Upgrade**: Upgraded Shakapacker from 9.1.0 to 9.2.0. This minor version update adds a new `bin/shakapacker-config` utility for debugging webpack/rspack configurations with doctor mode, save mode, and stdout mode options. Supports YAML, JSON, and Node.js inspect output formats. by [justin808](https://github.com/justin808). -- **Removed Pro Warning Badge**: Removed the visual warning badge that appeared when non-Pro users attempted to enable Pro-only features like `immediate_hydration`. Pro features are now silently disabled when a Pro license is not available, providing a cleaner user experience without intrusive warning banners. [PR 1993](https://github.com/shakacode/react_on_rails/pull/1993) by [AbanoubGhadban](https://github.com/AbanoubGhadban). +- **Removed Pro Warning Badge**: Removed the visual warning badge that appeared when non-Pro users attempted to enable Pro-only features like `immediate_hydration`. Pro features are now silently disabled when a Pro license is not available, providing a cleaner user experience without intrusive warning banners. [PR #1993](https://github.com/shakacode/react_on_rails/pull/1993) by [AbanoubGhadban](https://github.com/AbanoubGhadban). - **`immediate_hydration` now automatically enabled for Pro users**: The `config.immediate_hydration` configuration option has been removed. Immediate hydration is now automatically enabled for React on Rails Pro users and disabled for non-Pro users, simplifying configuration while providing optimal performance by default. Component-level overrides are still supported via the `immediate_hydration` parameter on `react_component`, `redux_store`, and `stream_react_component` helpers. [PR 1997](https://github.com/shakacode/react_on_rails/pull/1997) by [AbanoubGhadban](https://github.com/AbanoubGhadban). diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index 2fb826f5f5..0741a6eba2 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -66,8 +66,18 @@ class Configuration :component_registry_timeout, :server_bundle_output_path, :enforce_private_server_bundles + # Class instance variable to track if deprecation warning has been shown + @immediate_hydration_warned = false + + class << self + attr_accessor :immediate_hydration_warned + end + # Deprecated: immediate_hydration configuration has been removed def immediate_hydration=(value) + return if self.class.immediate_hydration_warned + + self.class.immediate_hydration_warned = true Rails.logger.warn <<~WARNING [REACT ON RAILS] The 'config.immediate_hydration' configuration option is deprecated and no longer used. Immediate hydration is now automatically enabled for React on Rails Pro users. @@ -77,6 +87,9 @@ def immediate_hydration=(value) end def immediate_hydration + return nil if self.class.immediate_hydration_warned + + self.class.immediate_hydration_warned = true Rails.logger.warn <<~WARNING [REACT ON RAILS] The 'config.immediate_hydration' configuration option is deprecated and no longer used. Immediate hydration is now automatically enabled for React on Rails Pro users. diff --git a/lib/react_on_rails/utils.rb b/lib/react_on_rails/utils.rb index 3d3177c964..a9d60acbff 100644 --- a/lib/react_on_rails/utils.rb +++ b/lib/react_on_rails/utils.rb @@ -27,16 +27,20 @@ def self.immediate_hydration_pro_license_warning(name, type = "Component") # @param name [String] The name of the component/store (for warning messages) # @param type [String] The type ("Component" or "Store") for warning messages # @return [Boolean] The normalized immediate_hydration value + # @raise [ArgumentError] If value is not a boolean or nil # # Logic: + # - Validates that value is true, false, or nil # - If value is explicitly true (boolean) and no Pro license: warn and return false # - If value is nil: return true for Pro users, false for non-Pro users # - Otherwise: return the value as-is (allows explicit false to work) - # - # Note: We check for `== true` (not truthy) to only trigger on explicit boolean true, - # not on strings like "yes" or other truthy values which should be rejected by Ruby's - # type system at the call site. def self.normalize_immediate_hydration(value, name, type = "Component") + # Type validation: only accept boolean or nil + unless [true, false, nil].include?(value) + raise ArgumentError, + "[REACT ON RAILS] immediate_hydration must be true, false, or nil. Got: #{value.inspect} (#{value.class})" + end + # Strict equality check: only trigger warning for explicit boolean true if value == true && !react_on_rails_pro? Rails.logger.warn immediate_hydration_pro_license_warning(name, type) diff --git a/spec/react_on_rails/react_component/render_options_spec.rb b/spec/react_on_rails/react_component/render_options_spec.rb index 59c494b004..5c6aa15836 100644 --- a/spec/react_on_rails/react_component/render_options_spec.rb +++ b/spec/react_on_rails/react_component/render_options_spec.rb @@ -241,6 +241,20 @@ def the_attrs(react_component_name: "App", options: {}) expect(opts.immediate_hydration).to be false end end + + context "with invalid immediate_hydration value" do + it "raises ArgumentError for non-boolean values" do + options = { immediate_hydration: "yes" } + attrs = the_attrs(options: options) + + opts = described_class.new(**attrs) + + expect { opts.immediate_hydration }.to raise_error( + ArgumentError, + /immediate_hydration must be true, false, or nil/ + ) + end + end end end end From 0d8552b18b90ddfbd0a59d7bfcb088b717654b1a Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Wed, 12 Nov 2025 16:26:48 -1000 Subject: [PATCH 16/17] Implement code review recommendations: improve thread-safety, tests, and deprecation detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comprehensive unit tests for ReactOnRails::Utils.normalize_immediate_hydration - Tests for Pro/non-Pro license scenarios - Tests for invalid value type validation - Tests for warning messages with component/store names - Add tests for deprecated config.immediate_hydration setter/getter - Tests for deprecation warnings on both setter and getter - Tests for warning suppression (only warns once) - Tests for setter/getter interaction scenarios - Fix thread-safety of deprecation warning flag - Implement Mutex-based synchronization for @immediate_hydration_warned flag - Ensures safe access in multi-threaded environments (Puma, Sidekiq, etc.) - Add doctor.rb detection for deprecated config.immediate_hydration - Helps users identify and remove deprecated configuration during migration - Provides clear guidance on automatic behavior for Pro users - Remove unnecessary blank line in controller.rb for cleaner code All tests passing. Zero RuboCop violations. šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/react_on_rails/configuration.rb | 24 +++-- lib/react_on_rails/controller.rb | 1 - lib/react_on_rails/doctor.rb | 12 ++- spec/react_on_rails/configuration_spec.rb | 112 ++++++++++++++++++++++ spec/react_on_rails/utils_spec.rb | 94 ++++++++++++++++++ 5 files changed, 234 insertions(+), 9 deletions(-) diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index 0741a6eba2..6fafb9a7a7 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -66,18 +66,25 @@ class Configuration :component_registry_timeout, :server_bundle_output_path, :enforce_private_server_bundles - # Class instance variable to track if deprecation warning has been shown + # Class instance variable and mutex to track if deprecation warning has been shown + # Using mutex to ensure thread-safety in multi-threaded environments @immediate_hydration_warned = false + @immediate_hydration_mutex = Mutex.new class << self - attr_accessor :immediate_hydration_warned + attr_accessor :immediate_hydration_warned, :immediate_hydration_mutex end # Deprecated: immediate_hydration configuration has been removed def immediate_hydration=(value) - return if self.class.immediate_hydration_warned + warned = false + self.class.immediate_hydration_mutex.synchronize do + warned = self.class.immediate_hydration_warned + self.class.immediate_hydration_warned = true unless warned + end + + return if warned - self.class.immediate_hydration_warned = true Rails.logger.warn <<~WARNING [REACT ON RAILS] The 'config.immediate_hydration' configuration option is deprecated and no longer used. Immediate hydration is now automatically enabled for React on Rails Pro users. @@ -87,9 +94,14 @@ def immediate_hydration=(value) end def immediate_hydration - return nil if self.class.immediate_hydration_warned + warned = false + self.class.immediate_hydration_mutex.synchronize do + warned = self.class.immediate_hydration_warned + self.class.immediate_hydration_warned = true unless warned + end + + return nil if warned - self.class.immediate_hydration_warned = true Rails.logger.warn <<~WARNING [REACT ON RAILS] The 'config.immediate_hydration' configuration option is deprecated and no longer used. Immediate hydration is now automatically enabled for React on Rails Pro users. diff --git a/lib/react_on_rails/controller.rb b/lib/react_on_rails/controller.rb index 62736558d3..dae80a838b 100644 --- a/lib/react_on_rails/controller.rb +++ b/lib/react_on_rails/controller.rb @@ -16,7 +16,6 @@ module Controller # or else there will be no client side hydration of your stores. def redux_store(store_name, props: {}, immediate_hydration: nil) immediate_hydration = ReactOnRails::Utils.normalize_immediate_hydration(immediate_hydration, store_name, "Store") - redux_store_data = { store_name: store_name, props: props, immediate_hydration: immediate_hydration } diff --git a/lib/react_on_rails/doctor.rb b/lib/react_on_rails/doctor.rb index 4d12977055..efe5f80006 100644 --- a/lib/react_on_rails/doctor.rb +++ b/lib/react_on_rails/doctor.rb @@ -701,7 +701,7 @@ def analyze_server_rendering_config(content) end # rubocop:enable Metrics/AbcSize - # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity def analyze_performance_config(content) checker.add_info("\n⚔ Performance & Loading:") @@ -732,13 +732,21 @@ def analyze_performance_config(content) auto_load_match = content.match(/config\.auto_load_bundle\s*=\s*([^\s\n,]+)/) checker.add_info(" auto_load_bundle: #{auto_load_match[1]}") if auto_load_match + # Deprecated immediate_hydration setting + immediate_hydration_match = content.match(/config\.immediate_hydration\s*=\s*([^\s\n,]+)/) + if immediate_hydration_match + checker.add_warning(" āš ļø immediate_hydration: #{immediate_hydration_match[1]} (DEPRECATED)") + checker.add_info(" šŸ’” This setting is no longer used. Immediate hydration is now automatic for Pro users.") + checker.add_info(" šŸ’” Remove this line from your config/initializers/react_on_rails.rb file.") + end + # Component registry timeout timeout_match = content.match(/config\.component_registry_timeout\s*=\s*([^\s\n,]+)/) return unless timeout_match checker.add_info(" component_registry_timeout: #{timeout_match[1]}ms") end - # rubocop:enable Metrics/CyclomaticComplexity + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity # rubocop:disable Metrics/AbcSize def analyze_development_config(content) diff --git a/spec/react_on_rails/configuration_spec.rb b/spec/react_on_rails/configuration_spec.rb index e3123fcdb0..5cf82806de 100644 --- a/spec/react_on_rails/configuration_spec.rb +++ b/spec/react_on_rails/configuration_spec.rb @@ -366,6 +366,118 @@ module ReactOnRails end end + describe ".immediate_hydration (deprecated)" do + before do + # Reset the warning flag before each test + described_class.immediate_hydration_warned = false + allow(Rails.logger).to receive(:warn) + end + + after do + # Reset the warning flag after each test + described_class.immediate_hydration_warned = false + end + + describe "setter" do + it "logs a deprecation warning when setting to true" do + ReactOnRails.configure do |config| + config.immediate_hydration = true + end + + expect(Rails.logger).to have_received(:warn) + .with(/immediate_hydration' configuration option is deprecated/) + end + + it "logs a deprecation warning when setting to false" do + ReactOnRails.configure do |config| + config.immediate_hydration = false + end + + expect(Rails.logger).to have_received(:warn) + .with(/immediate_hydration' configuration option is deprecated/) + end + + it "mentions the value in the warning message" do + ReactOnRails.configure do |config| + config.immediate_hydration = true + end + + expect(Rails.logger).to have_received(:warn) do |message| + expect(message).to include("config.immediate_hydration = true") + end + end + + it "only logs the warning once even if called multiple times" do + ReactOnRails.configure do |config| + config.immediate_hydration = true + config.immediate_hydration = false + config.immediate_hydration = true + end + + expect(Rails.logger).to have_received(:warn).once + end + end + + describe "getter" do + it "logs a deprecation warning when accessed" do + ReactOnRails.configure {} # rubocop:disable Lint/EmptyBlock + + ReactOnRails.configuration.immediate_hydration + + expect(Rails.logger).to have_received(:warn) + .with(/immediate_hydration' configuration option is deprecated/) + end + + it "returns nil" do + ReactOnRails.configure {} # rubocop:disable Lint/EmptyBlock + + result = ReactOnRails.configuration.immediate_hydration + + expect(result).to be_nil + end + + it "only logs the warning once even if called multiple times" do + ReactOnRails.configure {} # rubocop:disable Lint/EmptyBlock + + ReactOnRails.configuration.immediate_hydration + ReactOnRails.configuration.immediate_hydration + ReactOnRails.configuration.immediate_hydration + + expect(Rails.logger).to have_received(:warn).once + end + end + + describe "setter and getter interactions" do + it "does not warn again on getter if setter already warned" do + ReactOnRails.configure do |config| + config.immediate_hydration = true + end + + expect(Rails.logger).to have_received(:warn).once + + ReactOnRails.configuration.immediate_hydration + + # Still only one warning total + expect(Rails.logger).to have_received(:warn).once + end + + it "does not warn again on setter if getter already warned" do + ReactOnRails.configure {} # rubocop:disable Lint/EmptyBlock + + ReactOnRails.configuration.immediate_hydration + + expect(Rails.logger).to have_received(:warn).once + + ReactOnRails.configure do |config| + config.immediate_hydration = true + end + + # Still only one warning total + expect(Rails.logger).to have_received(:warn).once + end + end + end + describe "enforce_private_server_bundles validation" do context "when enforce_private_server_bundles is true" do before do diff --git a/spec/react_on_rails/utils_spec.rb b/spec/react_on_rails/utils_spec.rb index b43f4d9444..908f00d958 100644 --- a/spec/react_on_rails/utils_spec.rb +++ b/spec/react_on_rails/utils_spec.rb @@ -898,6 +898,100 @@ def self.configuration=(config) end # RSC utility method tests moved to react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb + + describe ".normalize_immediate_hydration" do + context "with Pro license" do + before do + allow(described_class).to receive(:react_on_rails_pro?).and_return(true) + end + + it "returns true when value is explicitly true" do + result = described_class.normalize_immediate_hydration(true, "TestComponent", "Component") + expect(result).to be true + end + + it "returns false when value is explicitly false" do + result = described_class.normalize_immediate_hydration(false, "TestComponent", "Component") + expect(result).to be false + end + + it "returns true when value is nil (Pro default)" do + result = described_class.normalize_immediate_hydration(nil, "TestComponent", "Component") + expect(result).to be true + end + + it "does not log a warning for any valid value" do + expect(Rails.logger).not_to receive(:warn) + + described_class.normalize_immediate_hydration(true, "TestComponent", "Component") + described_class.normalize_immediate_hydration(false, "TestComponent", "Component") + described_class.normalize_immediate_hydration(nil, "TestComponent", "Component") + end + end + + context "without Pro license" do + before do + allow(described_class).to receive(:react_on_rails_pro?).and_return(false) + end + + it "returns false and logs warning when value is explicitly true" do + expect(Rails.logger).to receive(:warn) + .with(/immediate_hydration: true requires a React on Rails Pro license/) + + result = described_class.normalize_immediate_hydration(true, "TestComponent", "Component") + expect(result).to be false + end + + it "returns false when value is explicitly false" do + expect(Rails.logger).not_to receive(:warn) + + result = described_class.normalize_immediate_hydration(false, "TestComponent", "Component") + expect(result).to be false + end + + it "returns false when value is nil (non-Pro default)" do + expect(Rails.logger).not_to receive(:warn) + + result = described_class.normalize_immediate_hydration(nil, "TestComponent", "Component") + expect(result).to be false + end + + it "includes component name and type in warning message" do + expect(Rails.logger).to receive(:warn) do |message| + expect(message).to include("TestStore") + expect(message).to include("Store") + end + + described_class.normalize_immediate_hydration(true, "TestStore", "Store") + end + end + + context "with invalid values" do + it "raises ArgumentError for string values" do + expect do + described_class.normalize_immediate_hydration("yes", "TestComponent", "Component") + end.to raise_error(ArgumentError, /immediate_hydration must be true, false, or nil/) + end + + it "raises ArgumentError for numeric values" do + expect do + described_class.normalize_immediate_hydration(1, "TestComponent", "Component") + end.to raise_error(ArgumentError, /immediate_hydration must be true, false, or nil/) + end + + it "raises ArgumentError for hash values" do + expect do + described_class.normalize_immediate_hydration({}, "TestComponent", "Component") + end.to raise_error(ArgumentError, /immediate_hydration must be true, false, or nil/) + end + + it "includes the invalid value in error message" do + expect do + described_class.normalize_immediate_hydration("invalid", "TestComponent", "Component") + end.to raise_error(ArgumentError, /Got: "invalid" \(String\)/) + end + end + end end end # rubocop:enable Metrics/ModuleLength, Metrics/BlockLength From 5c18ac9b67e7749707fc291cfb4632da765a303f Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Wed, 12 Nov 2025 16:56:58 -1000 Subject: [PATCH 17/17] Make generated_component_packs_loading_strategy default based on Pro license MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pro users (Shakapacker >= 8.2.0): defaults to :async for optimal performance - Non-Pro users (Shakapacker >= 8.2.0): defaults to :defer for compatibility - Older Shakapacker versions: continues to default to :sync (unchanged) This aligns with the immediate_hydration behavior where Pro features are automatically enabled based on license detection, providing the best default experience for each user type. Updated tests to verify Pro/non-Pro default behaviors separately while maintaining tests for explicit value overrides. šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CHANGELOG.md | 2 ++ lib/react_on_rails/configuration.rb | 3 ++- spec/react_on_rails/configuration_spec.rb | 23 ++++++++++++++++++++--- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d94dd6c84c..44983d884a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,8 @@ Changes since the last non-beta release. - **`immediate_hydration` now automatically enabled for Pro users**: The `config.immediate_hydration` configuration option has been removed. Immediate hydration is now automatically enabled for React on Rails Pro users and disabled for non-Pro users, simplifying configuration while providing optimal performance by default. Component-level overrides are still supported via the `immediate_hydration` parameter on `react_component`, `redux_store`, and `stream_react_component` helpers. [PR 1997](https://github.com/shakacode/react_on_rails/pull/1997) by [AbanoubGhadban](https://github.com/AbanoubGhadban). +- **`generated_component_packs_loading_strategy` now defaults based on Pro license**: When using Shakapacker >= 8.2.0, the default loading strategy is now `:async` for Pro users and `:defer` for non-Pro users. This provides optimal performance for Pro users while maintaining compatibility for non-Pro users. You can still explicitly set the strategy in your configuration. [PR #1993](https://github.com/shakacode/react_on_rails/pull/1993) by [AbanoubGhadban](https://github.com/AbanoubGhadban). + #### Bug Fixes - **Use as Git dependency**: All packages can now be installed as Git dependencies. This is useful for development and testing purposes. See [CONTRIBUTING.md](./CONTRIBUTING.md#git-dependencies) for documentation. [PR #1873](https://github.com/shakacode/react_on_rails/pull/1873) by [alexeyr-ci2](https://github.com/alexeyr-ci2). diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index 6fafb9a7a7..bff1d20bca 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -219,7 +219,8 @@ def validate_generated_component_packs_loading_strategy 2. Upgrade to Shakapacker v8.2.0 or above to enable async script loading MSG if PackerUtils.supports_async_loading? - self.generated_component_packs_loading_strategy ||= :async + # Default based on Pro license: Pro users get :async, non-Pro users get :defer + self.generated_component_packs_loading_strategy ||= (Utils.react_on_rails_pro? ? :async : :defer) elsif generated_component_packs_loading_strategy.nil? Rails.logger.warn("**WARNING** #{msg}") self.generated_component_packs_loading_strategy = :sync diff --git a/spec/react_on_rails/configuration_spec.rb b/spec/react_on_rails/configuration_spec.rb index 5cf82806de..7d90b7ffcc 100644 --- a/spec/react_on_rails/configuration_spec.rb +++ b/spec/react_on_rails/configuration_spec.rb @@ -284,9 +284,26 @@ module ReactOnRails .with("8.2.0").and_return(true) end - it "defaults to :async" do - ReactOnRails.configure {} # rubocop:disable Lint/EmptyBlock - expect(ReactOnRails.configuration.generated_component_packs_loading_strategy).to eq(:async) + context "with Pro license" do + before do + allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(true) + end + + it "defaults to :async" do + ReactOnRails.configure {} # rubocop:disable Lint/EmptyBlock + expect(ReactOnRails.configuration.generated_component_packs_loading_strategy).to eq(:async) + end + end + + context "without Pro license" do + before do + allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(false) + end + + it "defaults to :defer" do + ReactOnRails.configure {} # rubocop:disable Lint/EmptyBlock + expect(ReactOnRails.configuration.generated_component_packs_loading_strategy).to eq(:defer) + end end it "accepts :async value" do