From 4c3c9aa3b704e85e596a8cd8d24a85ecf1922a15 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 9 Mar 2025 18:30:59 +0200 Subject: [PATCH 01/15] Add configurable loading strategy for generated component packs - Replace `defer_generated_component_packs` with more flexible `generated_component_packs_loading_strategy` - Add support for `:async`, `:defer`, and `:sync` loading strategies - Validate loading strategy based on Shakapacker version - Update helper to support new loading strategy configuration - Add comprehensive specs for the new configuration option --- lib/react_on_rails/configuration.rb | 37 +++++++- lib/react_on_rails/helper.rb | 11 ++- .../helpers/react_on_rails_helper_spec.rb | 39 ++++++++ spec/react_on_rails/configuration_spec.rb | 91 ++++++++++++++++++- 4 files changed, 170 insertions(+), 8 deletions(-) diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index 3226f3a13..af4325913 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -43,13 +43,13 @@ def self.configuration i18n_output_format: nil, components_subdirectory: nil, make_generated_server_bundle_the_entrypoint: false, - defer_generated_component_packs: false, # forces the loading of React components force_load: true, # 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. - component_registry_timeout: DEFAULT_COMPONENT_REGISTRY_TIMEOUT + component_registry_timeout: DEFAULT_COMPONENT_REGISTRY_TIMEOUT, + generated_component_packs_loading_strategy: nil ) end @@ -64,7 +64,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, - :defer_generated_component_packs, :force_load, :rsc_bundle_js_file, + :generated_component_packs_loading_strategy, :force_load, :rsc_bundle_js_file, :react_client_manifest_file, :component_registry_timeout # rubocop:disable Metrics/AbcSize @@ -76,7 +76,7 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender skip_display_none: nil, generated_assets_dirs: nil, generated_assets_dir: nil, webpack_generated_files: nil, rendering_extension: nil, build_test_command: nil, - build_production_command: nil, defer_generated_component_packs: nil, + build_production_command: nil, generated_component_packs_loading_strategy: nil, 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, @@ -122,8 +122,8 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender self.components_subdirectory = components_subdirectory self.auto_load_bundle = auto_load_bundle self.make_generated_server_bundle_the_entrypoint = make_generated_server_bundle_the_entrypoint - self.defer_generated_component_packs = defer_generated_component_packs self.force_load = force_load + self.generated_component_packs_loading_strategy = generated_component_packs_loading_strategy end # rubocop:enable Metrics/AbcSize @@ -139,6 +139,7 @@ def setup_config_values # check_deprecated_settings adjust_precompile_task check_component_registry_timeout + validate_generated_component_packs_loading_strategy end private @@ -151,6 +152,32 @@ def check_component_registry_timeout raise ReactOnRails::Error, "component_registry_timeout must be a positive integer" end + def validate_generated_component_packs_loading_strategy + if PackerUtils.shakapacker_version_requirement_met?([8, 2, 0]) + self.generated_component_packs_loading_strategy ||= :async + elsif generated_component_packs_loading_strategy.nil? + msg = <<~MSG + **WARNING** ReactOnRails: Your current version of #{ReactOnRails::PackerUtils.packer_type.upcase_first} \ + does not support async script loading which may cause performance issues. Please upgrade to Shakapacker v8.2.0 \ + or above to enable async script loading for better performance. + MSG + Rails.logger.warn(msg) + self.generated_component_packs_loading_strategy = :sync + elsif generated_component_packs_loading_strategy == :async + msg = <<~MSG + **ERROR** ReactOnRails: Your current version of #{ReactOnRails::PackerUtils.packer_type.upcase_first} \ + does not support async script loading. Please either: + 1. Use :sync or :defer loading strategy instead of :async + 2. Upgrade to Shakapacker v8.2.0 or above to enable async script loading + MSG + raise ReactOnRails::Error, msg + end + + return if [:async, :defer, :sync].include?(generated_component_packs_loading_strategy) + + raise ReactOnRails::Error, "generated_component_packs_loading_strategy must be either :async, :defer, or :sync" + end + def check_autobundling_requirements raise_missing_components_subdirectory if auto_load_bundle && !components_subdirectory.present? return unless components_subdirectory.present? diff --git a/lib/react_on_rails/helper.rb b/lib/react_on_rails/helper.rb index a2f7116a2..0126a5c32 100644 --- a/lib/react_on_rails/helper.rb +++ b/lib/react_on_rails/helper.rb @@ -422,8 +422,15 @@ def load_pack_for_generated_component(react_component_name, render_options) is_component_pack_present = File.exist?(generated_components_pack_path(react_component_name)) raise_missing_autoloaded_bundle(react_component_name) unless is_component_pack_present end - append_javascript_pack_tag("generated/#{react_component_name}", - defer: ReactOnRails.configuration.defer_generated_component_packs) + + options = { defer: ReactOnRails.configuration.generated_component_packs_loading_strategy == :defer } + # Old versions of Shakapacker don't support async script tags. + # ReactOnRails.configure already validates if async loading is supported by the installed Shakapacker version. + # Therefore, we only need to pass the async option if the loading strategy is explicitly set to :async + if ReactOnRails.configuration.generated_component_packs_loading_strategy == :async + options[:async] = true + end + append_javascript_pack_tag("generated/#{react_component_name}", **options) append_stylesheet_pack_tag("generated/#{react_component_name}") 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 5d247de48..1c5409dd1 100644 --- a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb +++ b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb @@ -54,6 +54,45 @@ class PlainReactOnRailsHelper expect(helper).to have_received(:append_stylesheet_pack_tag).with("generated/component_name") end + context "when async loading is enabled" do + before do + allow(ReactOnRails.configuration).to receive(:generated_component_packs_loading_strategy).and_return(:async) + end + + it "appends the async attribute to the script tag" do + original_append_javascript_pack_tag = helper.method(:append_javascript_pack_tag) + begin + # Temporarily redefine append_javascript_pack_tag to handle the async keyword argument. + # This is needed because older versions of Shakapacker (which may be used during testing) don't support async loading, + # but we still want to test that the async option is passed correctly when enabled. + def helper.append_javascript_pack_tag(name, **options) + original_append_javascript_pack_tag.call(name, **options) + end + allow(helper).to receive(:append_javascript_pack_tag) + allow(helper).to receive(:append_stylesheet_pack_tag) + expect { helper.load_pack_for_generated_component("component_name", render_options) }.not_to raise_error + expect(helper).to have_received(:append_javascript_pack_tag).with("generated/component_name", { defer: false, async: true }) + expect(helper).to have_received(:append_stylesheet_pack_tag).with("generated/component_name") + ensure + helper.define_singleton_method(:append_javascript_pack_tag, original_append_javascript_pack_tag) + end + end + end + + context "when defer loading is enabled" do + before do + allow(ReactOnRails.configuration).to receive(:generated_component_packs_loading_strategy).and_return(:defer) + end + + it "appends the defer attribute to the script tag" do + allow(helper).to receive(:append_javascript_pack_tag) + allow(helper).to receive(:append_stylesheet_pack_tag) + expect { helper.load_pack_for_generated_component("component_name", render_options) }.not_to raise_error + expect(helper).to have_received(:append_javascript_pack_tag).with("generated/component_name", { defer: true }) + expect(helper).to have_received(:append_stylesheet_pack_tag).with("generated/component_name") + end + end + it "throws an error in development if generated component isn't found" do allow(Rails.env).to receive(:development?).and_return(true) expect { helper.load_pack_for_generated_component("nonexisting_component", render_options) } diff --git a/spec/react_on_rails/configuration_spec.rb b/spec/react_on_rails/configuration_spec.rb index 84d978422..1c53b8041 100644 --- a/spec/react_on_rails/configuration_spec.rb +++ b/spec/react_on_rails/configuration_spec.rb @@ -265,7 +265,7 @@ module ReactOnRails end expect(ReactOnRails::PackerUtils).to have_received(:using_packer?).thrice - expect(ReactOnRails::PackerUtils).to have_received(:shakapacker_version_requirement_met?) + expect(ReactOnRails::PackerUtils).to have_received(:shakapacker_version_requirement_met?).twice expect(ReactOnRails::PackerUtils).to have_received(:nested_entries?) end @@ -277,6 +277,95 @@ module ReactOnRails expect(ReactOnRails.configuration.random_dom_id).to be(true) end + + describe ".generated_component_packs_loading_strategy" do + context "when using Shakapacker >= 8.2.0" do + before do + allow(ReactOnRails::PackerUtils).to receive(:shakapacker_version_requirement_met?) + .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) + end + + it "accepts :async value" do + expect do + ReactOnRails.configure do |config| + config.generated_component_packs_loading_strategy = :async + end + end.not_to raise_error + expect(ReactOnRails.configuration.generated_component_packs_loading_strategy).to eq(:async) + end + + it "accepts :defer value" do + expect do + ReactOnRails.configure do |config| + config.generated_component_packs_loading_strategy = :defer + end + end.not_to raise_error + expect(ReactOnRails.configuration.generated_component_packs_loading_strategy).to eq(:defer) + end + + it "accepts :sync value" do + expect do + ReactOnRails.configure do |config| + config.generated_component_packs_loading_strategy = :sync + end + end.not_to raise_error + expect(ReactOnRails.configuration.generated_component_packs_loading_strategy).to eq(:sync) + end + + it "raises error for invalid values" do + expect do + ReactOnRails.configure do |config| + config.generated_component_packs_loading_strategy = :invalid + end + end.to raise_error(ReactOnRails::Error, /must be either :async, :defer, or :sync/) + end + end + + context "when using Shakapacker < 8.2.0" do + before do + allow(ReactOnRails::PackerUtils).to receive(:shakapacker_version_requirement_met?) + .with([8, 2, 0]).and_return(false) + allow(Rails.logger).to receive(:warn) + end + + it "defaults to :sync and logs a warning" do + ReactOnRails.configure {} # rubocop:disable Lint/EmptyBlock + expect(ReactOnRails.configuration.generated_component_packs_loading_strategy).to eq(:sync) + expect(Rails.logger).to have_received(:warn).with(/does not support async script loading/) + end + + it "accepts :defer value" do + expect do + ReactOnRails.configure do |config| + config.generated_component_packs_loading_strategy = :defer + end + end.not_to raise_error + expect(ReactOnRails.configuration.generated_component_packs_loading_strategy).to eq(:defer) + end + + it "accepts :sync value" do + expect do + ReactOnRails.configure do |config| + config.generated_component_packs_loading_strategy = :sync + end + end.not_to raise_error + expect(ReactOnRails.configuration.generated_component_packs_loading_strategy).to eq(:sync) + end + + it "raises error for :async value" do + expect do + ReactOnRails.configure do |config| + config.generated_component_packs_loading_strategy = :async + end + end.to raise_error(ReactOnRails::Error, /does not support async script loading/) + end + end + end end end From d13299b4cc185e50075245b7ee30adb94b4cfab0 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 9 Mar 2025 18:53:54 +0200 Subject: [PATCH 02/15] linting --- lib/react_on_rails/configuration.rb | 2 +- lib/react_on_rails/helper.rb | 4 +--- spec/dummy/spec/helpers/react_on_rails_helper_spec.rb | 10 +++++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index af4325913..91317239b 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -173,7 +173,7 @@ def validate_generated_component_packs_loading_strategy raise ReactOnRails::Error, msg end - return if [:async, :defer, :sync].include?(generated_component_packs_loading_strategy) + return if %i[async defer sync].include?(generated_component_packs_loading_strategy) raise ReactOnRails::Error, "generated_component_packs_loading_strategy must be either :async, :defer, or :sync" end diff --git a/lib/react_on_rails/helper.rb b/lib/react_on_rails/helper.rb index 0126a5c32..6534fede9 100644 --- a/lib/react_on_rails/helper.rb +++ b/lib/react_on_rails/helper.rb @@ -427,9 +427,7 @@ def load_pack_for_generated_component(react_component_name, render_options) # Old versions of Shakapacker don't support async script tags. # ReactOnRails.configure already validates if async loading is supported by the installed Shakapacker version. # Therefore, we only need to pass the async option if the loading strategy is explicitly set to :async - if ReactOnRails.configuration.generated_component_packs_loading_strategy == :async - options[:async] = true - end + options[:async] = true if ReactOnRails.configuration.generated_component_packs_loading_strategy == :async append_javascript_pack_tag("generated/#{react_component_name}", **options) append_stylesheet_pack_tag("generated/#{react_component_name}") 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 1c5409dd1..c10653059 100644 --- a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb +++ b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb @@ -63,15 +63,19 @@ class PlainReactOnRailsHelper original_append_javascript_pack_tag = helper.method(:append_javascript_pack_tag) begin # Temporarily redefine append_javascript_pack_tag to handle the async keyword argument. - # This is needed because older versions of Shakapacker (which may be used during testing) don't support async loading, - # but we still want to test that the async option is passed correctly when enabled. + # This is needed because older versions of Shakapacker (which may be used during testing) + # don't support async loading, but we still want to test that the async option is passed + # correctly when enabled. def helper.append_javascript_pack_tag(name, **options) original_append_javascript_pack_tag.call(name, **options) end allow(helper).to receive(:append_javascript_pack_tag) allow(helper).to receive(:append_stylesheet_pack_tag) expect { helper.load_pack_for_generated_component("component_name", render_options) }.not_to raise_error - expect(helper).to have_received(:append_javascript_pack_tag).with("generated/component_name", { defer: false, async: true }) + expect(helper).to have_received(:append_javascript_pack_tag).with( + "generated/component_name", + { defer: false, async: true } + ) expect(helper).to have_received(:append_stylesheet_pack_tag).with("generated/component_name") ensure helper.define_singleton_method(:append_javascript_pack_tag, original_append_javascript_pack_tag) From ea771caec29289d1519f1650abbad0c19229a3c0 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Mon, 10 Mar 2025 13:10:57 +0200 Subject: [PATCH 03/15] Update Shakapacker version --- Gemfile.development_dependencies | 3 ++- Gemfile.lock | 22 ++++++++++++++-------- lib/react_on_rails/configuration.rb | 3 ++- spec/dummy/Gemfile.lock | 22 ++++++++++++++-------- spec/dummy/package.json | 2 +- spec/dummy/yarn.lock | 8 ++++---- 6 files changed, 37 insertions(+), 23 deletions(-) diff --git a/Gemfile.development_dependencies b/Gemfile.development_dependencies index ea7f695b9..e9dd0ef7d 100644 --- a/Gemfile.development_dependencies +++ b/Gemfile.development_dependencies @@ -1,6 +1,7 @@ # frozen_string_literal: true -gem "shakapacker", "8.0.0" +# TODO: Revert before merging +gem "shakapacker", github: "shakacode/shakapacker", branch: "abanoubghadban/pro521-add-support-for-async-scripts-in-the-view" gem "bootsnap", require: false gem "rails", "~> 7.1" diff --git a/Gemfile.lock b/Gemfile.lock index 98b5df8bc..4280eb6ad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,15 @@ +GIT + remote: https://github.com/shakacode/shakapacker.git + revision: 01462be63865356d9e3127e9f7422ae4348f7b5c + branch: abanoubghadban/pro521-add-support-for-async-scripts-in-the-view + specs: + shakapacker (8.1.0) + activesupport (>= 5.2) + package_json + rack-proxy (>= 0.6.1) + railties (>= 5.2) + semantic_range (>= 2.3.0) + PATH remote: . specs: @@ -336,13 +348,7 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - semantic_range (3.0.0) - shakapacker (8.0.0) - activesupport (>= 5.2) - package_json - rack-proxy (>= 0.6.1) - railties (>= 5.2) - semantic_range (>= 2.3.0) + semantic_range (3.1.0) simplecov (0.16.1) docile (~> 1.1) json (>= 1.8, < 3) @@ -431,7 +437,7 @@ DEPENDENCIES scss_lint sdoc selenium-webdriver (= 4.9.0) - shakapacker (= 8.0.0) + shakapacker! spring (~> 4.0) sprockets (~> 4.0) sqlite3 (~> 1.6) diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index 91317239b..eb2ed1ece 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -153,7 +153,8 @@ def check_component_registry_timeout end def validate_generated_component_packs_loading_strategy - if PackerUtils.shakapacker_version_requirement_met?([8, 2, 0]) + # TODO: Use version 8.2.0 after it's released and before merging this branch + if PackerUtils.shakapacker_version_requirement_met?([8, 1, 0]) self.generated_component_packs_loading_strategy ||= :async elsif generated_component_packs_loading_strategy.nil? msg = <<~MSG diff --git a/spec/dummy/Gemfile.lock b/spec/dummy/Gemfile.lock index b4ed7eb77..74c1db7df 100644 --- a/spec/dummy/Gemfile.lock +++ b/spec/dummy/Gemfile.lock @@ -1,3 +1,15 @@ +GIT + remote: https://github.com/shakacode/shakapacker.git + revision: 01462be63865356d9e3127e9f7422ae4348f7b5c + branch: abanoubghadban/pro521-add-support-for-async-scripts-in-the-view + specs: + shakapacker (8.1.0) + activesupport (>= 5.2) + package_json + rack-proxy (>= 0.6.1) + railties (>= 5.2) + semantic_range (>= 2.3.0) + PATH remote: ../.. specs: @@ -331,13 +343,7 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - semantic_range (3.0.0) - shakapacker (8.0.0) - activesupport (>= 5.2) - package_json - rack-proxy (>= 0.6.1) - railties (>= 5.2) - semantic_range (>= 2.3.0) + semantic_range (3.1.0) simplecov (0.16.1) docile (~> 1.1) json (>= 1.8, < 3) @@ -423,7 +429,7 @@ DEPENDENCIES scss_lint sdoc selenium-webdriver (= 4.9.0) - shakapacker (= 8.0.0) + shakapacker! spring (~> 4.0) sprockets (~> 4.0) sqlite3 (~> 1.6) diff --git a/spec/dummy/package.json b/spec/dummy/package.json index b7a3688a5..9e0227d07 100644 --- a/spec/dummy/package.json +++ b/spec/dummy/package.json @@ -50,7 +50,7 @@ "sass": "^1.43.4", "sass-loader": "^12.3.0", "sass-resources-loader": "^2.1.0", - "shakapacker": "8.0.0", + "shakapacker": "8.1.0", "style-loader": "^3.3.1", "terser-webpack-plugin": "5.3.1", "url-loader": "^4.0.0", diff --git a/spec/dummy/yarn.lock b/spec/dummy/yarn.lock index 19bdf1c19..15e81f469 100644 --- a/spec/dummy/yarn.lock +++ b/spec/dummy/yarn.lock @@ -6068,10 +6068,10 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" -shakapacker@8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/shakapacker/-/shakapacker-8.0.0.tgz#f29537c19078af7318758c92e7a1bca4cee96bdd" - integrity sha512-HCdpITzIKXzGEyUWQhKzPbpwwOsgTamaPH+0kXdhM59VQxZ3NWnT5cL3DlJdAT3sGsWCJskEl3eMkQlnh9DjhA== +shakapacker@8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/shakapacker/-/shakapacker-8.1.0.tgz#cb9f318f70cd59a99d3a46dda478aede657034c2" + integrity sha512-2OCl9KoKraW7CbUUfcEmTt/hK7IfOJx2h6LwsqEUUMsCcMWv3aFpZPjp5B74VdJxIqTgZo/8XVYKI0mj+itnxA== dependencies: js-yaml "^4.1.0" path-complete-extname "^1.0.0" From 0c9117f0b3412273210104a7de0fd8bb68b2da65 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Mon, 10 Mar 2025 13:13:45 +0200 Subject: [PATCH 04/15] Update CHANGELOG and release notes of v15.0.0 --- CHANGELOG.md | 6 ++++++ docs/release-notes/15.0.0.md | 21 ++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14410aa35..6339019af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,12 @@ After a release, please make sure to run `bundle exec rake update_changelog`. Th Changes since the last non-beta release. +#### Added +- Configuration option `generated_component_packs_loading_strategy` to control how generated component packs are loaded. It supports `sync`, `async`, and `defer` strategies. [PR 1712](https://github.com/shakacode/react_on_rails/pull/1712) by [AbanoubGhadban](https://github.com/AbanoubGhadban). + +### Removed (Breaking Changes) +- Removed `defer_generated_component_packs` configuration option. You can use `generated_component_packs_loading_strategy` instead. [PR 1712](https://github.com/shakacode/react_on_rails/pull/1712) by [AbanoubGhadban](https://github.com/AbanoubGhadban). + ### [15.0.0-alpha.2] - 2025-03-07 See [Release Notes](docs/release-notes/15.0.0.md) for full details. diff --git a/docs/release-notes/15.0.0.md b/docs/release-notes/15.0.0.md index 216162940..00a53a723 100644 --- a/docs/release-notes/15.0.0.md +++ b/docs/release-notes/15.0.0.md @@ -21,15 +21,25 @@ Major improvements to component and store hydration: - Can use `async` scripts in the page with no fear of race condition - No need to use `defer` anymore +### Enhanced Script Loading Strategies +- New configuration option `generated_component_packs_loading_strategy` replaces `defer_generated_component_packs` +- Supports three loading strategies: + - `:async` - Loads scripts asynchronously (default for Shakapacker ≥ 8.2.0) + - `:defer` - Defers script execution until after page load (Doesn't work good with Streamed HTML as it will wait for the full page load before hydrating the components) + - `:sync` - Loads scripts synchronously (default for Shakapacker < 8.2.0) (Better to upgrade to Shakapacker 8.2.0 and use `:async` strategy) +- Improves page performance by optimizing how component packs are loaded + ## Breaking Changes ### Component Hydration Changes -- The `defer_generated_component_packs` and `force_load` configurations now default to `false` and `true` respectively. This means components will hydrate early without waiting for the full page load. This improves performance by eliminating unnecessary delays in hydration. - +- The `defer_generated_component_packs` configuration has been removed. Use `generated_component_packs_loading_strategy` instead. +- The `generated_component_packs_loading_strategy` defaults to `:async` for Shakapacker ≥ 8.2.0 and `:sync` for Shakapacker < 8.2.0. +- The `force_load` configuration now defaults to `true`. +- The new default values of `generated_component_packs_loading_strategy: :async` and `force_load: true` work together to optimize component hydration. Components now hydrate as soon as their code and server-rendered HTML are available, without waiting for the full page to load. This parallel processing significantly improves time-to-interactive by eliminating the traditional waterfall of waiting for page load before beginning hydration (It's critical for streamed HTML). - The previous need for deferring scripts to prevent race conditions has been eliminated due to improved hydration handling. Making scripts not defer is critical to execute the hydration scripts early before the page is fully loaded. - The `force_load` configuration makes `react-on-rails` hydrate components immediately as soon as their server-rendered HTML reaches the client, without waiting for the full page load. - - If you want to keep the previous behavior, you can set `defer_generated_component_packs: true` or `force_load: false` in your `config/initializers/react_on_rails.rb` file. + - If you want to keep the previous behavior, you can set `generated_component_packs_loading_strategy: :defer` or `force_load: false` in your `config/initializers/react_on_rails.rb` file. - You can also keep it for individual components by passing `force_load: false` to `react_component` or `stream_react_component`. - Redux store now supports `force_load` option, which defaults to `config.force_load` (and so to `true` if that isn't set). If `true`, the Redux store will hydrate immediately as soon as its server-side data reaches the client. - You can override this behavior for individual Redux stores by calling the `redux_store` helper with `force_load: false`, same as `react_component`. @@ -50,6 +60,11 @@ Major improvements to component and store hydration: - If you call it in a `turbolinks:load` listener to work around the issue documented in [Turbolinks](https://www.shakacode.com/react-on-rails/docs/rails/turbolinks/#async-script-loading), the listener can be safely removed. +### Script Loading Strategy Migration +- If you were previously using `defer_generated_component_packs: true`, use `generated_component_packs_loading_strategy: :defer` instead +- If you were previously using `defer_generated_component_packs: false`, use `generated_component_packs_loading_strategy: :sync` instead +- For optimal performance with Shakapacker ≥ 8.2.0, consider using `generated_component_packs_loading_strategy: :async` + ## Store Dependencies for Components When using Redux stores with multiple components, you need to explicitly declare store dependencies to optimize hydration. Here's how: From 2e920887db03758277519cb622c8cb17fca60388 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Mon, 10 Mar 2025 13:24:45 +0200 Subject: [PATCH 05/15] Update generated component pack loading strategy test for CI compatibility --- spec/dummy/spec/helpers/react_on_rails_helper_spec.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 c10653059..b1e539e64 100644 --- a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb +++ b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb @@ -50,7 +50,13 @@ class PlainReactOnRailsHelper allow(helper).to receive(:append_javascript_pack_tag) allow(helper).to receive(:append_stylesheet_pack_tag) expect { helper.load_pack_for_generated_component("component_name", render_options) }.not_to raise_error - expect(helper).to have_received(:append_javascript_pack_tag).with("generated/component_name", { defer: false }) + + if ENV["CI_PACKER_VERSION"] == "old" + expect(helper).to have_received(:append_javascript_pack_tag).with("generated/component_name", { defer: false }) + else + expect(helper).to have_received(:append_javascript_pack_tag) + .with("generated/component_name",{ defer: false, async: true }) + end expect(helper).to have_received(:append_stylesheet_pack_tag).with("generated/component_name") end From bb240e7ec46359accc24b6b9ee8f7db74eac2129 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Mon, 10 Mar 2025 14:27:05 +0200 Subject: [PATCH 06/15] Update Shakapacker version requirement in configuration specs --- spec/dummy/spec/helpers/react_on_rails_helper_spec.rb | 2 +- spec/react_on_rails/configuration_spec.rb | 6 ++++-- 2 files changed, 5 insertions(+), 3 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 b1e539e64..8b7866ebc 100644 --- a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb +++ b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb @@ -55,7 +55,7 @@ class PlainReactOnRailsHelper expect(helper).to have_received(:append_javascript_pack_tag).with("generated/component_name", { defer: false }) else expect(helper).to have_received(:append_javascript_pack_tag) - .with("generated/component_name",{ defer: false, async: true }) + .with("generated/component_name", { defer: false, async: true }) end expect(helper).to have_received(:append_stylesheet_pack_tag).with("generated/component_name") end diff --git a/spec/react_on_rails/configuration_spec.rb b/spec/react_on_rails/configuration_spec.rb index 1c53b8041..da1f3c164 100644 --- a/spec/react_on_rails/configuration_spec.rb +++ b/spec/react_on_rails/configuration_spec.rb @@ -282,7 +282,8 @@ module ReactOnRails context "when using Shakapacker >= 8.2.0" do before do allow(ReactOnRails::PackerUtils).to receive(:shakapacker_version_requirement_met?) - .with([8, 2, 0]).and_return(true) + # TODO: Set to 8.2.0 after it's released and before merging this branch + .with([8, 1, 0]).and_return(true) end it "defaults to :async" do @@ -329,7 +330,8 @@ module ReactOnRails context "when using Shakapacker < 8.2.0" do before do allow(ReactOnRails::PackerUtils).to receive(:shakapacker_version_requirement_met?) - .with([8, 2, 0]).and_return(false) + # TODO: Set to 8.2.0 after it's released and before merging this branch + .with([8, 1, 0]).and_return(false) allow(Rails.logger).to receive(:warn) end From 5b461d92fa98f37a5ed06013d29964bb81dd3d6a Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Mon, 10 Mar 2025 15:10:57 +0200 Subject: [PATCH 07/15] Add force_load option to redux_store method at ReactOnRails::Controller --- lib/react_on_rails/controller.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/react_on_rails/controller.rb b/lib/react_on_rails/controller.rb index bf0a08a23..440f808ed 100644 --- a/lib/react_on_rails/controller.rb +++ b/lib/react_on_rails/controller.rb @@ -12,9 +12,11 @@ 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: {}) + def redux_store(store_name, props: {}, force_load: nil) + force_load = ReactOnRails.configuration.force_load if force_load.nil? redux_store_data = { store_name: store_name, - props: props } + props: props, + force_load: force_load } @registered_stores_defer_render ||= [] @registered_stores_defer_render << redux_store_data end From a398f226c4a4d864521230e86ce39e719758613e Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Mon, 10 Mar 2025 15:13:39 +0200 Subject: [PATCH 08/15] Add packer version environment variable to CI workflows --- .github/workflows/examples.yml | 3 +++ .github/workflows/main.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 0ea114e7b..a44d7c7f9 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -95,6 +95,9 @@ jobs: fi - name: Increase the amount of inotify watchers run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p + - name: Set packer version environment variable + run: | + echo "CI_PACKER_VERSION=${{ matrix.versions == 'oldest' && 'old' || 'new' }}" >> $GITHUB_ENV - name: Main CI if: steps.changed-files.outputs.any_changed == 'true' run: bundle exec rake run_rspec:${{ matrix.versions == 'oldest' && 'web' || 'shaka' }}packer_examples diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3c0489f78..e4a1c1d2e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -190,6 +190,9 @@ jobs: git config user.name "Your Name" git commit -am "stop generators from complaining about uncommitted code" - run: cd spec/dummy && bundle info shakapacker + - name: Set packer version environment variable + run: | + echo "CI_PACKER_VERSION=${{ matrix.versions == 'oldest' && 'old' || 'new' }}" >> $GITHUB_ENV - name: Main CI run: bundle exec rake run_rspec:all_dummy - name: Store test results From b37cd9707e028cf81874c7e2aef9905e7f76f7e2 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 13 Mar 2025 11:05:46 +0200 Subject: [PATCH 09/15] Update Shakapacker to version 8.2.0 instead of using git dependency --- Gemfile.development_dependencies | 3 +-- Gemfile.lock | 20 +++++++------------- lib/react_on_rails/configuration.rb | 3 +-- spec/dummy/Gemfile.lock | 20 +++++++------------- spec/dummy/package.json | 2 +- spec/dummy/yarn.lock | 8 ++++---- spec/react_on_rails/configuration_spec.rb | 6 ++---- 7 files changed, 23 insertions(+), 39 deletions(-) diff --git a/Gemfile.development_dependencies b/Gemfile.development_dependencies index e9dd0ef7d..1b4814c94 100644 --- a/Gemfile.development_dependencies +++ b/Gemfile.development_dependencies @@ -1,7 +1,6 @@ # frozen_string_literal: true -# TODO: Revert before merging -gem "shakapacker", github: "shakacode/shakapacker", branch: "abanoubghadban/pro521-add-support-for-async-scripts-in-the-view" +gem "shakapacker", "8.2.0" gem "bootsnap", require: false gem "rails", "~> 7.1" diff --git a/Gemfile.lock b/Gemfile.lock index 4280eb6ad..2bfef9bff 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,15 +1,3 @@ -GIT - remote: https://github.com/shakacode/shakapacker.git - revision: 01462be63865356d9e3127e9f7422ae4348f7b5c - branch: abanoubghadban/pro521-add-support-for-async-scripts-in-the-view - specs: - shakapacker (8.1.0) - activesupport (>= 5.2) - package_json - rack-proxy (>= 0.6.1) - railties (>= 5.2) - semantic_range (>= 2.3.0) - PATH remote: . specs: @@ -349,6 +337,12 @@ GEM rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) semantic_range (3.1.0) + shakapacker (8.2.0) + activesupport (>= 5.2) + package_json + rack-proxy (>= 0.6.1) + railties (>= 5.2) + semantic_range (>= 2.3.0) simplecov (0.16.1) docile (~> 1.1) json (>= 1.8, < 3) @@ -437,7 +431,7 @@ DEPENDENCIES scss_lint sdoc selenium-webdriver (= 4.9.0) - shakapacker! + shakapacker (= 8.2.0) spring (~> 4.0) sprockets (~> 4.0) sqlite3 (~> 1.6) diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index eb2ed1ece..91317239b 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -153,8 +153,7 @@ def check_component_registry_timeout end def validate_generated_component_packs_loading_strategy - # TODO: Use version 8.2.0 after it's released and before merging this branch - if PackerUtils.shakapacker_version_requirement_met?([8, 1, 0]) + if PackerUtils.shakapacker_version_requirement_met?([8, 2, 0]) self.generated_component_packs_loading_strategy ||= :async elsif generated_component_packs_loading_strategy.nil? msg = <<~MSG diff --git a/spec/dummy/Gemfile.lock b/spec/dummy/Gemfile.lock index 74c1db7df..cc1191c13 100644 --- a/spec/dummy/Gemfile.lock +++ b/spec/dummy/Gemfile.lock @@ -1,15 +1,3 @@ -GIT - remote: https://github.com/shakacode/shakapacker.git - revision: 01462be63865356d9e3127e9f7422ae4348f7b5c - branch: abanoubghadban/pro521-add-support-for-async-scripts-in-the-view - specs: - shakapacker (8.1.0) - activesupport (>= 5.2) - package_json - rack-proxy (>= 0.6.1) - railties (>= 5.2) - semantic_range (>= 2.3.0) - PATH remote: ../.. specs: @@ -344,6 +332,12 @@ GEM rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) semantic_range (3.1.0) + shakapacker (8.2.0) + activesupport (>= 5.2) + package_json + rack-proxy (>= 0.6.1) + railties (>= 5.2) + semantic_range (>= 2.3.0) simplecov (0.16.1) docile (~> 1.1) json (>= 1.8, < 3) @@ -429,7 +423,7 @@ DEPENDENCIES scss_lint sdoc selenium-webdriver (= 4.9.0) - shakapacker! + shakapacker (= 8.2.0) spring (~> 4.0) sprockets (~> 4.0) sqlite3 (~> 1.6) diff --git a/spec/dummy/package.json b/spec/dummy/package.json index 9e0227d07..f15534d53 100644 --- a/spec/dummy/package.json +++ b/spec/dummy/package.json @@ -50,7 +50,7 @@ "sass": "^1.43.4", "sass-loader": "^12.3.0", "sass-resources-loader": "^2.1.0", - "shakapacker": "8.1.0", + "shakapacker": "8.2.0", "style-loader": "^3.3.1", "terser-webpack-plugin": "5.3.1", "url-loader": "^4.0.0", diff --git a/spec/dummy/yarn.lock b/spec/dummy/yarn.lock index 15e81f469..71996f4e2 100644 --- a/spec/dummy/yarn.lock +++ b/spec/dummy/yarn.lock @@ -6068,10 +6068,10 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" -shakapacker@8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/shakapacker/-/shakapacker-8.1.0.tgz#cb9f318f70cd59a99d3a46dda478aede657034c2" - integrity sha512-2OCl9KoKraW7CbUUfcEmTt/hK7IfOJx2h6LwsqEUUMsCcMWv3aFpZPjp5B74VdJxIqTgZo/8XVYKI0mj+itnxA== +shakapacker@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/shakapacker/-/shakapacker-8.2.0.tgz#c7bed87b8be2ae565cfe616f68552be545c77e14" + integrity sha512-Ct7BFqJVnKbxdqCzG+ja7Q6LPt/PlB7sSVBfG5jsAvmVCADM05cuoNwEgYNjFGKbDzHAxUqy5XgoI9Y030+JKQ== dependencies: js-yaml "^4.1.0" path-complete-extname "^1.0.0" diff --git a/spec/react_on_rails/configuration_spec.rb b/spec/react_on_rails/configuration_spec.rb index da1f3c164..1c53b8041 100644 --- a/spec/react_on_rails/configuration_spec.rb +++ b/spec/react_on_rails/configuration_spec.rb @@ -282,8 +282,7 @@ module ReactOnRails context "when using Shakapacker >= 8.2.0" do before do allow(ReactOnRails::PackerUtils).to receive(:shakapacker_version_requirement_met?) - # TODO: Set to 8.2.0 after it's released and before merging this branch - .with([8, 1, 0]).and_return(true) + .with([8, 2, 0]).and_return(true) end it "defaults to :async" do @@ -330,8 +329,7 @@ module ReactOnRails context "when using Shakapacker < 8.2.0" do before do allow(ReactOnRails::PackerUtils).to receive(:shakapacker_version_requirement_met?) - # TODO: Set to 8.2.0 after it's released and before merging this branch - .with([8, 1, 0]).and_return(false) + .with([8, 2, 0]).and_return(false) allow(Rails.logger).to receive(:warn) end From c1d10b60a590221fb360cb3011bf85aefa53bae3 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 13 Mar 2025 11:06:56 +0200 Subject: [PATCH 10/15] linting --- CHANGELOG.md | 2 ++ docs/release-notes/15.0.0.md | 3 +++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6339019af..40d0e0918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,9 +24,11 @@ After a release, please make sure to run `bundle exec rake update_changelog`. Th Changes since the last non-beta release. #### Added + - Configuration option `generated_component_packs_loading_strategy` to control how generated component packs are loaded. It supports `sync`, `async`, and `defer` strategies. [PR 1712](https://github.com/shakacode/react_on_rails/pull/1712) by [AbanoubGhadban](https://github.com/AbanoubGhadban). ### Removed (Breaking Changes) + - Removed `defer_generated_component_packs` configuration option. You can use `generated_component_packs_loading_strategy` instead. [PR 1712](https://github.com/shakacode/react_on_rails/pull/1712) by [AbanoubGhadban](https://github.com/AbanoubGhadban). ### [15.0.0-alpha.2] - 2025-03-07 diff --git a/docs/release-notes/15.0.0.md b/docs/release-notes/15.0.0.md index 00a53a723..efc466678 100644 --- a/docs/release-notes/15.0.0.md +++ b/docs/release-notes/15.0.0.md @@ -22,6 +22,7 @@ Major improvements to component and store hydration: - No need to use `defer` anymore ### Enhanced Script Loading Strategies + - New configuration option `generated_component_packs_loading_strategy` replaces `defer_generated_component_packs` - Supports three loading strategies: - `:async` - Loads scripts asynchronously (default for Shakapacker ≥ 8.2.0) @@ -37,6 +38,7 @@ Major improvements to component and store hydration: - The `generated_component_packs_loading_strategy` defaults to `:async` for Shakapacker ≥ 8.2.0 and `:sync` for Shakapacker < 8.2.0. - The `force_load` configuration now defaults to `true`. - The new default values of `generated_component_packs_loading_strategy: :async` and `force_load: true` work together to optimize component hydration. Components now hydrate as soon as their code and server-rendered HTML are available, without waiting for the full page to load. This parallel processing significantly improves time-to-interactive by eliminating the traditional waterfall of waiting for page load before beginning hydration (It's critical for streamed HTML). + - The previous need for deferring scripts to prevent race conditions has been eliminated due to improved hydration handling. Making scripts not defer is critical to execute the hydration scripts early before the page is fully loaded. - The `force_load` configuration makes `react-on-rails` hydrate components immediately as soon as their server-rendered HTML reaches the client, without waiting for the full page load. - If you want to keep the previous behavior, you can set `generated_component_packs_loading_strategy: :defer` or `force_load: false` in your `config/initializers/react_on_rails.rb` file. @@ -61,6 +63,7 @@ Major improvements to component and store hydration: - If you call it in a `turbolinks:load` listener to work around the issue documented in [Turbolinks](https://www.shakacode.com/react-on-rails/docs/rails/turbolinks/#async-script-loading), the listener can be safely removed. ### Script Loading Strategy Migration + - If you were previously using `defer_generated_component_packs: true`, use `generated_component_packs_loading_strategy: :defer` instead - If you were previously using `defer_generated_component_packs: false`, use `generated_component_packs_loading_strategy: :sync` instead - For optimal performance with Shakapacker ≥ 8.2.0, consider using `generated_component_packs_loading_strategy: :async` From 84456e0d4fdb519a36d75e5ff161dc0e88b89f4a Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 13 Mar 2025 11:10:03 +0200 Subject: [PATCH 11/15] Refactor CI packer version environment variable handling in workflows --- .github/workflows/examples.yml | 2 +- .github/workflows/main.yml | 2 +- .github/workflows/rspec-package-specs.yml | 2 +- spec/dummy/spec/helpers/react_on_rails_helper_spec.rb | 2 +- spec/react_on_rails/utils_spec.rb | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index a44d7c7f9..caa488ac3 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -97,7 +97,7 @@ jobs: run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p - name: Set packer version environment variable run: | - echo "CI_PACKER_VERSION=${{ matrix.versions == 'oldest' && 'old' || 'new' }}" >> $GITHUB_ENV + echo "CI_PACKER_VERSION=${{ matrix.versions }}" >> $GITHUB_ENV - name: Main CI if: steps.changed-files.outputs.any_changed == 'true' run: bundle exec rake run_rspec:${{ matrix.versions == 'oldest' && 'web' || 'shaka' }}packer_examples diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e4a1c1d2e..ea84f1cd7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -192,7 +192,7 @@ jobs: - run: cd spec/dummy && bundle info shakapacker - name: Set packer version environment variable run: | - echo "CI_PACKER_VERSION=${{ matrix.versions == 'oldest' && 'old' || 'new' }}" >> $GITHUB_ENV + echo "CI_PACKER_VERSION=${{ matrix.versions }}" >> $GITHUB_ENV - name: Main CI run: bundle exec rake run_rspec:all_dummy - name: Store test results diff --git a/.github/workflows/rspec-package-specs.yml b/.github/workflows/rspec-package-specs.yml index e0b511308..67010526d 100644 --- a/.github/workflows/rspec-package-specs.yml +++ b/.github/workflows/rspec-package-specs.yml @@ -49,7 +49,7 @@ jobs: git commit -am "stop generators from complaining about uncommitted code" - name: Set packer version environment variable run: | - echo "CI_PACKER_VERSION=${{ matrix.versions == 'oldest' && 'old' || 'new' }}" >> $GITHUB_ENV + echo "CI_PACKER_VERSION=${{ matrix.versions }}" >> $GITHUB_ENV - name: Run rspec tests run: bundle exec rspec spec/react_on_rails - name: Store test results 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 8b7866ebc..852772d45 100644 --- a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb +++ b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb @@ -51,7 +51,7 @@ class PlainReactOnRailsHelper allow(helper).to receive(:append_stylesheet_pack_tag) expect { helper.load_pack_for_generated_component("component_name", render_options) }.not_to raise_error - if ENV["CI_PACKER_VERSION"] == "old" + if ENV["CI_PACKER_VERSION"] == "oldest" expect(helper).to have_received(:append_javascript_pack_tag).with("generated/component_name", { defer: false }) else expect(helper).to have_received(:append_javascript_pack_tag) diff --git a/spec/react_on_rails/utils_spec.rb b/spec/react_on_rails/utils_spec.rb index 087306eae..98fe24a50 100644 --- a/spec/react_on_rails/utils_spec.rb +++ b/spec/react_on_rails/utils_spec.rb @@ -10,9 +10,9 @@ module ReactOnRails # If rspec tests are run locally, we want to test both packers. # If rspec tests are run in CI, we want to test the packer specified in the CI_PACKER_VERSION environment variable. # Check script/convert and .github/workflows/rspec-package-specs.yml for more details. - packers_to_test = if ENV["CI_PACKER_VERSION"] == "old" + packers_to_test = if ENV["CI_PACKER_VERSION"] == "oldest" ["webpacker"] - elsif ENV["CI_PACKER_VERSION"] == "new" + elsif ENV["CI_PACKER_VERSION"] == "newest" ["shakapacker"] else %w[shakapacker webpacker] From a10112af3c263ca4dd07698fc2d8ecad9cbaff08 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 13 Mar 2025 11:21:14 +0200 Subject: [PATCH 12/15] linting --- docs/release-notes/15.0.0.md | 2 +- spec/dummy/spec/helpers/react_on_rails_helper_spec.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/15.0.0.md b/docs/release-notes/15.0.0.md index efc466678..01e961c56 100644 --- a/docs/release-notes/15.0.0.md +++ b/docs/release-notes/15.0.0.md @@ -26,7 +26,7 @@ Major improvements to component and store hydration: - New configuration option `generated_component_packs_loading_strategy` replaces `defer_generated_component_packs` - Supports three loading strategies: - `:async` - Loads scripts asynchronously (default for Shakapacker ≥ 8.2.0) - - `:defer` - Defers script execution until after page load (Doesn't work good with Streamed HTML as it will wait for the full page load before hydrating the components) + - `:defer` - Defers script execution until after page load (Doesn't work well with Streamed HTML as it will wait for the full page load before hydrating the components) - `:sync` - Loads scripts synchronously (default for Shakapacker < 8.2.0) (Better to upgrade to Shakapacker 8.2.0 and use `:async` strategy) - Improves page performance by optimizing how component packs are loaded 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 852772d45..b1e9f7d29 100644 --- a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb +++ b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb @@ -75,6 +75,7 @@ class PlainReactOnRailsHelper def helper.append_javascript_pack_tag(name, **options) original_append_javascript_pack_tag.call(name, **options) end + allow(helper).to receive(:append_javascript_pack_tag) allow(helper).to receive(:append_stylesheet_pack_tag) expect { helper.load_pack_for_generated_component("component_name", render_options) }.not_to raise_error From 99c8f66412106c550de2a2f19f88c521798d1a3a Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Thu, 13 Mar 2025 12:30:29 +0200 Subject: [PATCH 13/15] Update docs/release-notes/15.0.0.md Co-authored-by: Alexey Romanov --- docs/release-notes/15.0.0.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/15.0.0.md b/docs/release-notes/15.0.0.md index 01e961c56..2402fd5f3 100644 --- a/docs/release-notes/15.0.0.md +++ b/docs/release-notes/15.0.0.md @@ -26,8 +26,8 @@ Major improvements to component and store hydration: - New configuration option `generated_component_packs_loading_strategy` replaces `defer_generated_component_packs` - Supports three loading strategies: - `:async` - Loads scripts asynchronously (default for Shakapacker ≥ 8.2.0) - - `:defer` - Defers script execution until after page load (Doesn't work well with Streamed HTML as it will wait for the full page load before hydrating the components) - - `:sync` - Loads scripts synchronously (default for Shakapacker < 8.2.0) (Better to upgrade to Shakapacker 8.2.0 and use `:async` strategy) + - `:defer` - Defers script execution until after page load (doesn't work well with Streamed HTML as it will wait for the full page load before hydrating the components) + - `:sync` - Loads scripts synchronously (default for Shakapacker < 8.2.0) (better to upgrade to Shakapacker 8.2.0 and use `:async` strategy) - Improves page performance by optimizing how component packs are loaded ## Breaking Changes From ae29eaf2adcb02ebcb5ee9975515a0d7d5115375 Mon Sep 17 00:00:00 2001 From: Judah Meek Date: Tue, 25 Mar 2025 14:01:10 -0500 Subject: [PATCH 14/15] deprecate instead of replace --- CHANGELOG.md | 2 +- docs/release-notes/15.0.0.md | 2 +- lib/react_on_rails/configuration.rb | 13 +++++++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40d0e0918..c94aba1da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ Changes since the last non-beta release. ### Removed (Breaking Changes) -- Removed `defer_generated_component_packs` configuration option. You can use `generated_component_packs_loading_strategy` instead. [PR 1712](https://github.com/shakacode/react_on_rails/pull/1712) by [AbanoubGhadban](https://github.com/AbanoubGhadban). +- Deprecated `defer_generated_component_packs` configuration option. You should use `generated_component_packs_loading_strategy` instead. [PR 1712](https://github.com/shakacode/react_on_rails/pull/1712) by [AbanoubGhadban](https://github.com/AbanoubGhadban). ### [15.0.0-alpha.2] - 2025-03-07 diff --git a/docs/release-notes/15.0.0.md b/docs/release-notes/15.0.0.md index 2402fd5f3..50722bcfd 100644 --- a/docs/release-notes/15.0.0.md +++ b/docs/release-notes/15.0.0.md @@ -34,7 +34,7 @@ Major improvements to component and store hydration: ### Component Hydration Changes -- The `defer_generated_component_packs` configuration has been removed. Use `generated_component_packs_loading_strategy` instead. +- The `defer_generated_component_packs` configuration has been deprecated. Use `generated_component_packs_loading_strategy` instead. - The `generated_component_packs_loading_strategy` defaults to `:async` for Shakapacker ≥ 8.2.0 and `:sync` for Shakapacker < 8.2.0. - The `force_load` configuration now defaults to `true`. - The new default values of `generated_component_packs_loading_strategy: :async` and `force_load: true` work together to optimize component hydration. Components now hydrate as soon as their code and server-rendered HTML are available, without waiting for the full page to load. This parallel processing significantly improves time-to-interactive by eliminating the traditional waterfall of waiting for page load before beginning hydration (It's critical for streamed HTML). diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index 91317239b..115a43a93 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -43,6 +43,7 @@ def self.configuration i18n_output_format: nil, components_subdirectory: nil, make_generated_server_bundle_the_entrypoint: false, + defer_generated_component_packs: false, # forces the loading of React components force_load: true, # Maximum time in milliseconds to wait for client-side component registration after page load. @@ -60,7 +61,7 @@ class Configuration :generated_assets_dirs, :generated_assets_dir, :components_subdirectory, :webpack_generated_files, :rendering_extension, :build_test_command, :build_production_command, :i18n_dir, :i18n_yml_dir, :i18n_output_format, - :i18n_yml_safe_load_options, + :i18n_yml_safe_load_options, :defer_generated_component_packs, :server_render_method, :random_dom_id, :auto_load_bundle, :same_bundle_for_client_and_server, :rendering_props_extension, :make_generated_server_bundle_the_entrypoint, @@ -70,7 +71,7 @@ class Configuration # 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, - trace: nil, development_mode: nil, + trace: nil, development_mode: nil, defer_generated_component_packs: nil, logging_on_server: nil, server_renderer_pool_size: nil, server_renderer_timeout: nil, raise_on_prerender_error: true, skip_display_none: nil, generated_assets_dirs: nil, @@ -122,6 +123,7 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender self.components_subdirectory = components_subdirectory self.auto_load_bundle = auto_load_bundle self.make_generated_server_bundle_the_entrypoint = make_generated_server_bundle_the_entrypoint + self.defer_generated_component_packs = defer_generated_component_packs self.force_load = force_load self.generated_component_packs_loading_strategy = generated_component_packs_loading_strategy end @@ -152,9 +154,16 @@ def check_component_registry_timeout raise ReactOnRails::Error, "component_registry_timeout must be a positive integer" end + # rubocop:disable Metrics/CyclomaticComplexity def validate_generated_component_packs_loading_strategy + # rubocop:enable Metrics/CyclomaticComplexity if PackerUtils.shakapacker_version_requirement_met?([8, 2, 0]) self.generated_component_packs_loading_strategy ||= :async + elsif defer_generated_component_packs + generated_component_packs_loading_strategy ||= :defer + Rails.logger.warn "[DEPRECATION] ReactOnRails: Use config." \ + "generated_component_packs_loading_strategy = :defer rather than " \ + "defer_generated_component_packs" elsif generated_component_packs_loading_strategy.nil? msg = <<~MSG **WARNING** ReactOnRails: Your current version of #{ReactOnRails::PackerUtils.packer_type.upcase_first} \ From 0c50b80fd81d5ce60fec22a90b50068b97ad9458 Mon Sep 17 00:00:00 2001 From: Judah Meek Date: Tue, 25 Mar 2025 14:21:45 -0500 Subject: [PATCH 15/15] simplify logic --- lib/react_on_rails/configuration.rb | 43 +++++++++++++++-------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index 115a43a93..48e81fdba 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -154,32 +154,35 @@ def check_component_registry_timeout raise ReactOnRails::Error, "component_registry_timeout must be a positive integer" end - # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def validate_generated_component_packs_loading_strategy - # rubocop:enable Metrics/CyclomaticComplexity + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + + if defer_generated_component_packs + if %i[async sync].include?(generated_component_packs_loading_strategy) + Rails.logger.warn "**WARNING** ReactOnRails: config.defer_generated_component_packs is " \ + "superseded by config.generated_component_packs_loading_strategy" + else + Rails.logger.warn "[DEPRECATION] ReactOnRails: Use config." \ + "generated_component_packs_loading_strategy = :defer rather than " \ + "defer_generated_component_packs" + self.generated_component_packs_loading_strategy ||= :defer + end + end + + msg = <<~MSG + ReactOnRails: Your current version of #{ReactOnRails::PackerUtils.packer_type.upcase_first} \ + does not support async script loading, which may cause performance issues. Please either: + 1. Use :sync or :defer loading strategy instead of :async + 2. Upgrade to Shakapacker v8.2.0 or above to enable async script loading + MSG if PackerUtils.shakapacker_version_requirement_met?([8, 2, 0]) self.generated_component_packs_loading_strategy ||= :async - elsif defer_generated_component_packs - generated_component_packs_loading_strategy ||= :defer - Rails.logger.warn "[DEPRECATION] ReactOnRails: Use config." \ - "generated_component_packs_loading_strategy = :defer rather than " \ - "defer_generated_component_packs" elsif generated_component_packs_loading_strategy.nil? - msg = <<~MSG - **WARNING** ReactOnRails: Your current version of #{ReactOnRails::PackerUtils.packer_type.upcase_first} \ - does not support async script loading which may cause performance issues. Please upgrade to Shakapacker v8.2.0 \ - or above to enable async script loading for better performance. - MSG - Rails.logger.warn(msg) + Rails.logger.warn("**WARNING** #{msg}") self.generated_component_packs_loading_strategy = :sync elsif generated_component_packs_loading_strategy == :async - msg = <<~MSG - **ERROR** ReactOnRails: Your current version of #{ReactOnRails::PackerUtils.packer_type.upcase_first} \ - does not support async script loading. Please either: - 1. Use :sync or :defer loading strategy instead of :async - 2. Upgrade to Shakapacker v8.2.0 or above to enable async script loading - MSG - raise ReactOnRails::Error, msg + raise ReactOnRails::Error, "**ERROR** #{msg}" end return if %i[async defer sync].include?(generated_component_packs_loading_strategy)