Skip to content

Commit 153a6ce

Browse files
Refactor Pro helper methods and introduce utility for license validation
- Removed immediate hydration badge handling from helper methods to streamline rendering logic. - Introduced `generate_component_script` and `generate_store_script` methods for improved script tag generation, including support for immediate hydration. - Added `ReactOnRails::Pro::Utils` module to manage Pro feature licensing checks and disable options if the license is not valid. - Updated `render_options` to incorporate Pro feature validation, ensuring proper handling of disabled options. This refactor enhances the clarity and maintainability of the Pro features while ensuring compliance with licensing requirements.
1 parent 58212db commit 153a6ce

File tree

4 files changed

+148
-82
lines changed

4 files changed

+148
-82
lines changed

lib/react_on_rails/helper.rb

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ def react_component(component_name, options = {})
6060
server_rendered_html = internal_result[:result]["html"]
6161
console_script = internal_result[:result]["consoleReplayScript"]
6262
render_options = internal_result[:render_options]
63-
badge = pro_warning_badge_if_needed(internal_result[:immediate_hydration_requested])
6463

6564
case server_rendered_html
6665
when String
@@ -70,7 +69,7 @@ def react_component(component_name, options = {})
7069
console_script: console_script,
7170
render_options: render_options
7271
)
73-
(badge + html).html_safe
72+
html.html_safe
7473
when Hash
7574
msg = <<~MSG
7675
Use react_component_hash (not react_component) to return a Hash to your ruby view code. See
@@ -217,7 +216,6 @@ def react_component_hash(component_name, options = {})
217216
server_rendered_html = internal_result[:result]["html"]
218217
console_script = internal_result[:result]["consoleReplayScript"]
219218
render_options = internal_result[:render_options]
220-
badge = pro_warning_badge_if_needed(internal_result[:immediate_hydration_requested])
221219

222220
if server_rendered_html.is_a?(String) && internal_result[:result]["hasErrors"]
223221
server_rendered_html = { COMPONENT_HTML_KEY => internal_result[:result]["html"] }
@@ -230,7 +228,7 @@ def react_component_hash(component_name, options = {})
230228
console_script: console_script,
231229
render_options: render_options
232230
)
233-
result[COMPONENT_HTML_KEY] = badge + result[COMPONENT_HTML_KEY]
231+
result[COMPONENT_HTML_KEY] = result[COMPONENT_HTML_KEY]
234232
result
235233
else
236234
msg = <<~MSG
@@ -259,8 +257,6 @@ def react_component_hash(component_name, options = {})
259257
# hydrate this store immediately instead of waiting for the page to load.
260258
def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil)
261259
immediate_hydration = ReactOnRails.configuration.immediate_hydration if immediate_hydration.nil?
262-
badge = pro_warning_badge_if_needed(immediate_hydration)
263-
immediate_hydration = false unless support_pro_features?
264260

265261
redux_store_data = { store_name: store_name,
266262
props: props,
@@ -272,7 +268,7 @@ def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil)
272268
else
273269
registered_stores << redux_store_data
274270
result = render_redux_store_data(redux_store_data)
275-
(badge + prepend_render_rails_context(result)).html_safe
271+
(prepend_render_rails_context(result)).html_safe
276272
end
277273
end
278274

@@ -647,23 +643,10 @@ def internal_react_component(react_component_name, options = {})
647643
# server has already rendered the HTML.
648644

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

654647
# Setup the page_loaded_js, which is the same regardless of prerendering or not!
655648
# The reason is that React is smart about not doing extra work if the server rendering did its job.
656-
component_specification_tag = content_tag(:script,
657-
json_safe_and_pretty(render_options.client_props).html_safe,
658-
type: "application/json",
659-
class: "js-react-on-rails-component",
660-
id: "js-react-on-rails-component-#{render_options.dom_id}",
661-
"data-component-name" => render_options.react_component_name,
662-
"data-trace" => (render_options.trace ? true : nil),
663-
"data-dom-id" => render_options.dom_id,
664-
"data-store-dependencies" => render_options.store_dependencies&.to_json)
665-
666-
component_specification_tag = apply_immediate_hydration_if_supported(component_specification_tag, render_options)
649+
component_specification_tag = generate_component_script(render_options)
667650

668651
load_pack_for_generated_component(react_component_name, render_options)
669652
# Create the HTML rendering part
@@ -673,17 +656,11 @@ def internal_react_component(react_component_name, options = {})
673656
render_options: render_options,
674657
tag: component_specification_tag,
675658
result: result,
676-
immediate_hydration_requested: immediate_hydration_requested
677659
}
678660
end
679661

680662
def render_redux_store_data(redux_store_data)
681-
store_hydration_data = content_tag(:script,
682-
json_safe_and_pretty(redux_store_data[:props]).html_safe,
683-
type: "application/json",
684-
"data-js-react-on-rails-store" => redux_store_data[:store_name].html_safe)
685-
686-
store_hydration_data = apply_store_immediate_hydration_if_supported(store_hydration_data, redux_store_data)
663+
store_hydration_data = generate_store_script(redux_store_data)
687664

688665
prepend_render_rails_context(store_hydration_data)
689666
end

lib/react_on_rails/pro/helper.rb

Lines changed: 86 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -14,72 +14,106 @@
1414
# * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
1515
# */
1616

17-
module ReactOnRails
18-
module Pro
19-
module Helper
20-
IMMEDIATE_HYDRATION_PRO_WARNING = "[REACT ON RAILS] The 'immediate_hydration' feature requires a " \
21-
"React on Rails Pro license. " \
22-
"Please visit https://shakacode.com/react-on-rails-pro to learn more."
17+
module ReactOnRails::Pro
18+
module Helper
19+
IMMEDIATE_HYDRATION_PRO_WARNING = "[REACT ON RAILS] The 'immediate_hydration' feature requires a " \
20+
"React on Rails Pro license. " \
21+
"Please visit https://shakacode.com/react-on-rails-pro to learn more."
2322

24-
# This method is responsible for generating the necessary attributes and script tags
25-
# for the immediate_hydration feature. It is enabled only when a valid
26-
# React on Rails Pro license is detected.
27-
def apply_immediate_hydration_if_supported(component_specification_tag, render_options)
28-
return component_specification_tag unless render_options.immediate_hydration && support_pro_features?
23+
# Generates the complete component specification script tag.
24+
# Handles both immediate hydration (Pro feature) and standard cases.
25+
def generate_component_script(render_options)
26+
# Setup the page_loaded_js, which is the same regardless of prerendering or not!
27+
# The reason is that React is smart about not doing extra work if the server rendering did its job.
28+
component_specification_tag = content_tag(:script,
29+
json_safe_and_pretty(render_options.client_props).html_safe,
30+
type: "application/json",
31+
class: "js-react-on-rails-component",
32+
id: "js-react-on-rails-component-#{render_options.dom_id}",
33+
"data-component-name" => render_options.react_component_name,
34+
"data-trace" => (render_options.trace ? true : nil),
35+
"data-dom-id" => render_options.dom_id,
36+
"data-store-dependencies" => render_options.store_dependencies&.to_json,
37+
"data-immediate-hydration" =>
38+
(render_options.immediate_hydration ? true : nil))
2939

30-
# Add data attribute
31-
component_specification_tag.gsub!("<script ", '<script data-immediate-hydration="true" ')
32-
33-
# Add immediate invocation script
34-
component_specification_tag.concat(
35-
content_tag(:script, %(
36-
typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
37-
).html_safe)
38-
)
40+
# Add immediate invocation script if immediate hydration is enabled
41+
spec_tag = if render_options.immediate_hydration
42+
# Escape dom_id for JavaScript context
43+
escaped_dom_id = escape_javascript(render_options.dom_id)
44+
immediate_script = content_tag(:script, %(
45+
typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{escaped_dom_id}');
46+
).html_safe)
47+
"#{component_specification_tag}\n#{immediate_script}"
48+
else
49+
component_specification_tag
3950
end
4051

41-
# Similar logic for redux_store
42-
def apply_store_immediate_hydration_if_supported(store_hydration_data, redux_store_data)
43-
return store_hydration_data unless redux_store_data[:immediate_hydration] && support_pro_features?
52+
pro_warning_badge = pro_warning_badge_if_needed(render_options.explicitly_disabled_pro_options)
53+
"#{pro_warning_badge}\n#{spec_tag}".html_safe
54+
end
4455

45-
# Add data attribute
46-
store_hydration_data.gsub!("<script ", '<script data-immediate-hydration="true" ')
56+
# Generates the complete store hydration script tag.
57+
# Handles both immediate hydration (Pro feature) and standard cases.
58+
def generate_store_script(redux_store_data)
59+
pro_options_check_result = ReactOnRails::Pro::Utils.disable_pro_render_options_if_not_licensed(redux_store_data)
60+
redux_store_data = pro_options_check_result[:raw_options]
61+
explicitly_disabled_pro_options = pro_options_check_result[:explicitly_disabled_pro_options]
4762

48-
# Add immediate invocation script
49-
store_hydration_data.concat(
50-
content_tag(:script, <<~JS.strip_heredoc.html_safe
51-
typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsStoreLoaded('#{redux_store_data[:store_name]}');
52-
JS
53-
)
63+
store_hydration_data = content_tag(:script,
64+
json_safe_and_pretty(redux_store_data[:props]).html_safe,
65+
type: "application/json",
66+
"data-js-react-on-rails-store" => redux_store_data[:store_name].html_safe,
67+
"data-immediate-hydration" =>
68+
(redux_store_data[:immediate_hydration] ? true : nil))
69+
70+
# Add immediate invocation script if immediate hydration is enabled and Pro license is valid
71+
store_hydration_scripts =if redux_store_data[:immediate_hydration]
72+
# Escape store_name for JavaScript context
73+
escaped_store_name = escape_javascript(redux_store_data[:store_name])
74+
immediate_script = content_tag(:script, <<~JS.strip_heredoc.html_safe
75+
typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsStoreLoaded('#{escaped_store_name}');
76+
JS
5477
)
78+
"#{store_hydration_data}\n#{immediate_script}"
79+
else
80+
store_hydration_data
5581
end
5682

57-
# Checks if React on Rails Pro features are available
58-
# @return [Boolean] true if Pro license is valid, false otherwise
59-
def support_pro_features?
60-
ReactOnRails::Utils.react_on_rails_pro_licence_valid?
61-
end
83+
pro_warning_badge = pro_warning_badge_if_needed(explicitly_disabled_pro_options)
84+
"#{pro_warning_badge}\n#{store_hydration_scripts}".html_safe
85+
end
6286

63-
def pro_warning_badge_if_needed(immediate_hydration)
64-
return "".html_safe unless immediate_hydration
65-
return "".html_safe if support_pro_features?
87+
def pro_warning_badge_if_needed(explicitly_disabled_pro_options)
88+
return "" unless explicitly_disabled_pro_options.any?
6689

67-
puts IMMEDIATE_HYDRATION_PRO_WARNING
68-
Rails.logger.warn IMMEDIATE_HYDRATION_PRO_WARNING
90+
disabled_features_message = disabled_pro_features_message(explicitly_disabled_pro_options)
91+
warning_message = "[REACT ON RAILS] #{disabled_features_message}" + "\n" +
92+
"Please visit https://shakacode.com/react-on-rails-pro to learn more."
93+
puts warning_message
94+
Rails.logger.warn warning_message
6995

70-
tooltip_text = "The 'immediate_hydration' feature requires a React on Rails Pro license. Click to learn more."
96+
tooltip_text = "#{disabled_features_message} Click to learn more."
7197

72-
badge_html = <<~HTML
73-
<a href="https://shakacode.com/react-on-rails-pro" target="_blank" rel="noopener noreferrer" title="#{tooltip_text}">
74-
<div style="position: fixed; top: 0; right: 0; width: 180px; height: 180px; overflow: hidden; z-index: 9999; pointer-events: none;">
75-
<div style="position: absolute; top: 50px; right: -40px; transform: rotate(45deg); background-color: rgba(220, 53, 69, 0.85); color: white; padding: 7px 40px; text-align: center; font-weight: bold; font-family: sans-serif; font-size: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.3); pointer-events: auto;">
76-
React On Rails Pro Required
77-
</div>
98+
badge_html = <<~HTML.strip
99+
<a href="https://shakacode.com/react-on-rails-pro" target="_blank" rel="noopener noreferrer" title="#{tooltip_text}">
100+
<div style="position: fixed; top: 0; right: 0; width: 180px; height: 180px; overflow: hidden; z-index: 9999; pointer-events: none;">
101+
<div style="position: absolute; top: 50px; right: -40px; transform: rotate(45deg); background-color: rgba(220, 53, 69, 0.85); color: white; padding: 7px 40px; text-align: center; font-weight: bold; font-family: sans-serif; font-size: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.3); pointer-events: auto;">
102+
React On Rails Pro Required
78103
</div>
79-
</a>
80-
HTML
81-
badge_html.strip.html_safe
82-
end
104+
</div>
105+
</a>
106+
HTML
107+
badge_html
108+
end
109+
110+
def disabled_pro_features_message(explicitly_disabled_pro_options)
111+
return "".html_safe unless explicitly_disabled_pro_options.any?
112+
113+
feature_list = explicitly_disabled_pro_options.join(', ')
114+
feature_word = explicitly_disabled_pro_options.size == 1 ? "feature" : "features"
115+
"The '#{feature_list}' #{feature_word} #{explicitly_disabled_pro_options.size == 1 ? 'requires' : 'require'} a " \
116+
"React on Rails Pro license. "
83117
end
84118
end
85119
end

lib/react_on_rails/pro/utils.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
# /*
4+
# * Copyright (c) 2025 Shakacode LLC
5+
# *
6+
# * This file is NOT licensed under the MIT (open source) license.
7+
# * It is part of the React on Rails Pro offering and is licensed separately.
8+
# *
9+
# * Unauthorized copying, modification, distribution, or use of this file,
10+
# * via any medium, is strictly prohibited without a valid license agreement
11+
# * from Shakacode LLC.
12+
# *
13+
# * For licensing terms, please see:
14+
# * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
15+
# */
16+
17+
module ReactOnRails::Pro
18+
module Utils
19+
PRO_ONLY_OPTIONS = %i[immediate_hydration].freeze
20+
21+
# Checks if React on Rails Pro features are available
22+
# @return [Boolean] true if Pro license is valid, false otherwise
23+
def self.support_pro_features?
24+
ReactOnRails::Utils.react_on_rails_pro_licence_valid?
25+
end
26+
27+
def self.disable_pro_render_options_if_not_licensed(raw_options)
28+
if support_pro_features?
29+
return {
30+
raw_options: raw_options,
31+
explicitly_disabled_pro_options: []
32+
}
33+
end
34+
35+
raw_options_after_disable = raw_options.dup
36+
37+
explicitly_disabled_pro_options = PRO_ONLY_OPTIONS.select do |option|
38+
# Use global configuration if it's not overridden in the options
39+
next ReactOnRails.configuration.send(option) if raw_options[option].nil?
40+
41+
raw_options[option]
42+
end
43+
explicitly_disabled_pro_options.each { |option| raw_options_after_disable[option] = false }
44+
45+
{
46+
raw_options: raw_options_after_disable,
47+
explicitly_disabled_pro_options: explicitly_disabled_pro_options
48+
}
49+
end
50+
end
51+
end

lib/react_on_rails/react_component/render_options.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "react_on_rails/utils"
4+
require "react_on_rails/pro/utils"
45

56
module ReactOnRails
67
module ReactComponent
@@ -14,10 +15,13 @@ class RenderOptions
1415
# TODO: remove the required for named params
1516
def initialize(react_component_name: required("react_component_name"), options: required("options"))
1617
@react_component_name = react_component_name.camelize
17-
@options = options
18+
19+
result = ReactOnRails::Pro::Utils.disable_pro_render_options_if_not_licensed(options)
20+
@options = result[:raw_options]
21+
@explicitly_disabled_pro_options = result[:explicitly_disabled_pro_options]
1822
end
1923

20-
attr_reader :react_component_name
24+
attr_reader :react_component_name, :explicitly_disabled_pro_options
2125

2226
def throw_js_errors
2327
options.fetch(:throw_js_errors, false)

0 commit comments

Comments
 (0)