- Sponsor
-
Notifications
You must be signed in to change notification settings - Fork 633
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for streaming rendered components using renderToPipeableStream
#1633
Changes from 1 commit
7e4d04b
4fc054c
4f95d2d
a651535
337c5c2
849a0c5
7ca17d1
8e1c42d
66d1cc2
b086123
5d29c97
80f463e
1d4a68e
d3a4c64
5b96a6a
f1c3dbf
7cebb2f
096231d
809322c
f6af606
8937c5f
bf3ef6c
ebf180d
b6d9fd5
2d49ce7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
…ream
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -91,6 +91,16 @@ def react_component(component_name, options = {}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def stream_react_component(component_name, options = {}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
options = options.merge(stream?: true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
result = internal_react_component(component_name, options) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
build_react_component_result_for_server_streamed_content( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
rendered_html_stream: result[:result], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
component_specification_tag: result[:tag], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
render_options: result[:render_options] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# react_component_hash is used to return multiple HTML strings for server rendering, such as for | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# adding meta-tags to a page. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# It is exactly like react_component except for the following: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -334,6 +344,10 @@ def generated_components_pack_path(component_name) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"#{ReactOnRails::PackerUtils.packer_source_entry_path}/generated/#{component_name}.js" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def get_content_tag_options_html_tag(render_options) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def build_react_component_result_for_server_rendered_string( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
server_rendered_html: required("server_rendered_html"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
component_specification_tag: required("component_specification_tag"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -361,6 +375,33 @@ def build_react_component_result_for_server_rendered_string( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
prepend_render_rails_context(result) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def build_react_component_result_for_server_streamed_content( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
rendered_html_stream: required("rendered_html_stream"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
component_specification_tag: required("component_specification_tag"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
render_options: required("render_options") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
alexeyr-ci marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
content_tag_options_html_tag = render_options.html_options[:tag] || 'div' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# The component_specification_tag is appended to the first chunk | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# We need to pass it early with the first chunk because it's needed in hydration | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# We need to make sure that client can hydrate the app early even before all components are streamed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
is_first_chunk = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
rendered_html_stream = rendered_html_stream.prepend { rails_context_if_not_already_rendered } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.prepend { "<#{content_tag_options_html_tag} id=\"#{render_options.dom_id}\">" } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.transform(&:html_safe) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
rendered_html_stream = rendered_html_stream.transform do |chunk| | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The StreamRequest and StreamDecorator classes allow us to transform the stream of http body chunks |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
is_first_chunk = false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if is_first_chunk | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return "#{chunk}\n#{component_specification_tag}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
chunk | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.append { "</#{content_tag_options_html_tag}>" } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.append { component_specification_tag } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# TODO: handle console logs | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+456
to
+457
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reminder: Handle console logs. The TODO comment indicates that handling console logs is pending. Do you want me to help with handling console logs or open a GitHub issue to track this task? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. help There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @justin808, to handle console logs in the
Here's a conceptual example of how you might implement this: def build_react_component_result_for_server_streamed_content(
rendered_html_stream: required("rendered_html_stream"),
component_specification_tag: required("component_specification_tag"),
render_options: required("render_options")
)
content_tag_options_html_tag = render_options.html_options[:tag] || "div"
is_first_chunk = true
console_logs = capture_console_logs do
# Your rendering logic here
end
rendered_html_stream = rendered_html_stream.transform do |chunk|
if is_first_chunk
is_first_chunk = false
html_content = <<-HTML
#{rails_context_if_not_already_rendered}
#{component_specification_tag}
<#{content_tag_options_html_tag} id="#{render_options.dom_id}">#{chunk}</#{content_tag_options_html_tag}>
<script>console.log(#{console_logs.to_json});</script>
HTML
next html_content.strip
end
chunk
end
rendered_html_stream.transform(&:html_safe)
end
def capture_console_logs
# Redirect console output to a buffer
logs = []
original_console_log = $stdout
$stdout = StringIO.new
yield
logs << $stdout.string
$stdout = original_console_log
logs
end This is a basic example and may need adjustments based on your specific setup and requirements. Let me know if you need further assistance or if you'd like to open a GitHub issue to track this task. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def build_react_component_result_for_server_rendered_hash( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
server_rendered_html: required("server_rendered_html"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
component_specification_tag: required("component_specification_tag"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -404,20 +445,22 @@ def compose_react_component_html_with_spec_and_console(component_specification_t | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
HTML | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# prepend the rails_context if not yet applied | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def prepend_render_rails_context(render_value) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return render_value if @rendered_rails_context | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def rails_context_if_not_already_rendered | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return "" if @rendered_rails_context | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
data = rails_context(server_side: false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@rendered_rails_context = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
rails_context_content = content_tag(:script, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
json_safe_and_pretty(data).html_safe, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
type: "application/json", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
id: "js-react-on-rails-context") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
content_tag(:script, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
json_safe_and_pretty(data).html_safe, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
type: "application/json", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
id: "js-react-on-rails-context") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+502
to
+513
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Memoize the Rails context script tag In Apply this diff to memoize the script tag: def rails_context_if_not_already_rendered
return @rails_context_script if @rendered_rails_context
data = rails_context(server_side: false)
@rendered_rails_context = true
- content_tag(:script,
- json_safe_and_pretty(data).html_safe,
- type: "application/json",
- id: "js-react-on-rails-context")
+ @rails_context_script = content_tag(:script,
+ json_safe_and_pretty(data).html_safe,
+ type: "application/json",
+ id: "js-react-on-rails-context")
+ @rails_context_script
end 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"#{rails_context_content}\n#{render_value}".html_safe | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# prepend the rails_context if not yet applied | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def prepend_render_rails_context(render_value) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
"#{rails_context_if_not_already_rendered}\n#{render_value}".html_safe | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
AbanoubGhadban marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def internal_react_component(react_component_name, options = {}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -520,6 +563,9 @@ def server_rendered_react_component(render_options) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
js_code: js_code) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# TODO: handle errors for streams | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return result if render_options.stream? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
justin808 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+620
to
+622
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Implement error handling for streaming components. The TODO comment indicates that error handling for streaming components is missing. This could lead to silent failures or unclear error messages in production. Consider implementing error handling similar to the non-streaming case. Would you like me to help design the error handling mechanism for streaming components? |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if result["hasErrors"] && render_options.raise_on_prerender_error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# We caught this exception on our backtrace handler | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
raise ReactOnRails::PrerenderError.new(component_name: react_component_name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -107,6 +107,10 @@ def set_option(key, value) | |
options[key] = value | ||
end | ||
|
||
def stream? | ||
options[:stream?] | ||
end | ||
Comment on lines
+110
to
+112
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider using The new Also, could you clarify why this method is public while some other option retrieval methods are private? If there's no specific reason for it to be public, consider making it private for encapsulation. Here's a suggested implementation: private
def stream?
retrieve_configuration_value_for(:stream?)
end This assumes you'll add a corresponding |
||
|
||
private | ||
|
||
attr_reader :options | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -92,6 +92,12 @@ def exec_server_render_js(js_code, render_options, js_evaluator = nil) | |
end | ||
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity | ||
|
||
# TODO: merge with exec_server_render_js | ||
def exec_server_render_streaming_js(js_code, render_options, js_evaluator = nil) | ||
js_evaluator ||= self | ||
js_evaluator.eval_streaming_js(js_code, render_options) | ||
end | ||
Comment on lines
+95
to
+99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider refactoring to reduce duplication and improve error handling. The new
Consider refactoring the method as follows: def exec_server_render_js(js_code, render_options, js_evaluator = nil, streaming: false)
js_evaluator ||= self
if render_options.trace
trace_js_code_used("Evaluating code to server render#{streaming ? ' (streaming)' : ''}.", js_code)
end
begin
result = streaming ? js_evaluator.eval_streaming_js(js_code, render_options) : js_evaluator.eval_js(js_code, render_options)
process_result(result, render_options) unless streaming
rescue StandardError => err
handle_error(err)
end
end
def exec_server_render_streaming_js(js_code, render_options, js_evaluator = nil)
exec_server_render_js(js_code, render_options, js_evaluator, streaming: true)
end This refactoring combines both methods, adds error handling and tracing for streaming, and maintains backwards compatibility. |
||
|
||
def trace_js_code_used(msg, js_code, file_name = "tmp/server-generated.js", force: false) | ||
return unless ReactOnRails.configuration.trace || force | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The result is of type StreamRequest