Skip to content

Commit 151c33b

Browse files
add support for streaming rendered component using renderToPipeableStream
1 parent 1949ae2 commit 151c33b

File tree

3 files changed

+64
-8
lines changed

3 files changed

+64
-8
lines changed

lib/react_on_rails/helper.rb

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ def react_component(component_name, options = {})
9191
end
9292
end
9393

94+
def stream_react_component(component_name, options = {})
95+
options = options.merge(stream?: true)
96+
result = internal_react_component(component_name, options)
97+
build_react_component_result_for_server_streamed_content(
98+
rendered_html_stream: result[:result],
99+
component_specification_tag: result[:tag],
100+
render_options: result[:render_options]
101+
)
102+
end
103+
94104
# react_component_hash is used to return multiple HTML strings for server rendering, such as for
95105
# adding meta-tags to a page.
96106
# It is exactly like react_component except for the following:
@@ -334,6 +344,10 @@ def generated_components_pack_path(react_component_name)
334344
"#{ReactOnRails::WebpackerUtils.webpacker_source_entry_path}/generated/#{react_component_name}.js"
335345
end
336346

347+
def get_content_tag_options_html_tag(render_options)
348+
349+
end
350+
337351
def build_react_component_result_for_server_rendered_string(
338352
server_rendered_html: required("server_rendered_html"),
339353
component_specification_tag: required("component_specification_tag"),
@@ -361,6 +375,33 @@ def build_react_component_result_for_server_rendered_string(
361375
prepend_render_rails_context(result)
362376
end
363377

378+
def build_react_component_result_for_server_streamed_content(
379+
rendered_html_stream: required("rendered_html_stream"),
380+
component_specification_tag: required("component_specification_tag"),
381+
render_options: required("render_options")
382+
)
383+
content_tag_options_html_tag = render_options.html_options[:tag] || 'div'
384+
# The component_specification_tag is appended to the first chunk
385+
# We need to pass it early with the first chunk because it's needed in hydration
386+
# We need to make sure that client can hydrate the app early even before all components are streamed
387+
is_first_chunk = true
388+
389+
rendered_html_stream = rendered_html_stream.prepend { rails_context_if_not_already_rendered }
390+
.prepend { "<#{content_tag_options_html_tag} id=\"#{render_options.dom_id}\">" }
391+
.transform(&:html_safe)
392+
393+
rendered_html_stream = rendered_html_stream.transform do |chunk|
394+
is_first_chunk = false
395+
if is_first_chunk
396+
return "#{chunk}\n#{component_specification_tag}"
397+
end
398+
chunk
399+
end
400+
.append { "</#{content_tag_options_html_tag}>" }
401+
.append { component_specification_tag }
402+
# TODO: handle console logs
403+
end
404+
364405
def build_react_component_result_for_server_rendered_hash(
365406
server_rendered_html: required("server_rendered_html"),
366407
component_specification_tag: required("component_specification_tag"),
@@ -404,20 +445,22 @@ def compose_react_component_html_with_spec_and_console(component_specification_t
404445
HTML
405446
end
406447

407-
# prepend the rails_context if not yet applied
408-
def prepend_render_rails_context(render_value)
409-
return render_value if @rendered_rails_context
448+
def rails_context_if_not_already_rendered
449+
return "" if @rendered_rails_context
410450

411451
data = rails_context(server_side: false)
412452

413453
@rendered_rails_context = true
414454

415-
rails_context_content = content_tag(:script,
416-
json_safe_and_pretty(data).html_safe,
417-
type: "application/json",
418-
id: "js-react-on-rails-context")
455+
content_tag(:script,
456+
json_safe_and_pretty(data).html_safe,
457+
type: "application/json",
458+
id: "js-react-on-rails-context")
459+
end
419460

420-
"#{rails_context_content}\n#{render_value}".html_safe
461+
# prepend the rails_context if not yet applied
462+
def prepend_render_rails_context(render_value)
463+
"#{rails_context_if_not_already_rendered}\n#{render_value}".html_safe
421464
end
422465

423466
def internal_react_component(react_component_name, options = {})
@@ -512,6 +555,9 @@ def server_rendered_react_component(render_options)
512555
js_code: js_code)
513556
end
514557

558+
# TODO: handle errors for streams
559+
return result if render_options.stream?
560+
515561
if result["hasErrors"] && render_options.raise_on_prerender_error
516562
# We caught this exception on our backtrace handler
517563
raise ReactOnRails::PrerenderError.new(component_name: react_component_name,

lib/react_on_rails/react_component/render_options.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ def set_option(key, value)
103103
options[key] = value
104104
end
105105

106+
def stream?
107+
options[:stream?]
108+
end
109+
106110
private
107111

108112
attr_reader :options

lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ def exec_server_render_js(js_code, render_options, js_evaluator = nil)
9292
end
9393
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity
9494

95+
# TODO: merge with exec_server_render_js
96+
def exec_server_render_streaming_js(js_code, render_options, js_evaluator = nil)
97+
js_evaluator ||= self
98+
js_evaluator.eval_streaming_js(js_code, render_options)
99+
end
100+
95101
def trace_js_code_used(msg, js_code, file_name = "tmp/server-generated.js", force: false)
96102
return unless ReactOnRails.configuration.trace || force
97103

0 commit comments

Comments
 (0)