Skip to content

Commit d2545c1

Browse files
committed
feat(ServerRendering) support per-request renderers
1 parent 7e06547 commit d2545c1

10 files changed

+76
-28
lines changed

lib/react/rails/component_mount.rb

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@ class ComponentMount
1111
attr_accessor :output_buffer
1212
mattr_accessor :camelize_props_switch
1313

14-
# ControllerLifecycle calls these hooks
14+
# {ControllerLifecycle} calls these hooks
1515
# You can use them in custom helper implementations
16-
def setup(env)
16+
def setup(controller)
17+
@controller = controller
1718
end
1819

19-
def teardown(env)
20+
def teardown(controller)
21+
if controller.__prerenderer
22+
React::ServerRendering.checkin_renderer(controller.__prerenderer)
23+
end
2024
end
2125

2226
# Render a UJS-type HTML tag annotated with data attributes, which
@@ -30,7 +34,7 @@ def react_component(name, props = {}, options = {}, &block)
3034

3135
prerender_options = options[:prerender]
3236
if prerender_options
33-
block = Proc.new{ concat React::ServerRendering.render(name, props, prerender_options) }
37+
block = Proc.new{ concat(prerender_component(name, props, prerender_options)) }
3438
end
3539

3640
html_options = options.reverse_merge(:data => {})
@@ -47,6 +51,30 @@ def react_component(name, props = {}, options = {}, &block)
4751

4852
content_tag(html_tag, '', html_options, &block)
4953
end
54+
55+
module ControllerHelpers
56+
extend ActiveSupport::Concern
57+
58+
included do
59+
# An instance of a server renderer, for use during this request
60+
attr_accessor :__prerenderer
61+
end
62+
63+
# If you want a per-request renderer, add this method as a before-action
64+
#
65+
# @example Having one renderer instance for each controller action
66+
# before_action :checkout_renderer
67+
def checkout_prerenderer
68+
self.__prerenderer = React::ServerRendering.checkout_renderer
69+
end
70+
end
71+
72+
private
73+
74+
def prerender_component(component_name, props, prerender_options)
75+
renderer = @controller.try(:__prerenderer) || React::ServerRendering
76+
renderer.render(component_name, props, prerender_options)
77+
end
5078
end
5179
end
5280
end

lib/react/rails/controller_lifecycle.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module React
22
module Rails
3+
# This module is included into ActionController so that
4+
# per-request hooks can be called in the view helper.
35
module ControllerLifecycle
46
extend ActiveSupport::Concern
57

@@ -12,12 +14,14 @@ module ControllerLifecycle
1214
attr_reader :__react_component_helper
1315
end
1416

17+
# Instantiate the ViewHelper implementation and call its #setup method
1518
def setup_react_component_helper
1619
new_helper = React::Rails::ViewHelper.helper_implementation_class.new
1720
new_helper.setup(self)
1821
@__react_component_helper = new_helper
1922
end
2023

24+
# Call the ViewHelper implementation's #teardown method
2125
def teardown_react_component_helper
2226
@__react_component_helper.teardown(self)
2327
end

lib/react/rails/railtie.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class Railtie < ::Rails::Railtie
3737

3838
ActiveSupport.on_load(:action_controller) do
3939
include ::React::Rails::ControllerLifecycle
40+
include ::React::Rails::ComponentMount::ControllerHelpers
4041
end
4142

4243
ActiveSupport.on_load(:action_view) do

lib/react/server_rendering.rb

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,26 @@ module ServerRendering
77
mattr_accessor :renderer, :renderer_options,
88
:pool_size, :pool_timeout
99

10+
# Discard the old ConnectionPool & create a new one
1011
def self.reset_pool
1112
options = {size: pool_size, timeout: pool_timeout}
12-
@@pool = ConnectionPool.new(options) { create_renderer }
13+
@@pool = ConnectionPool.new(options) { self.renderer.new(self.renderer_options) }
1314
end
1415

16+
# Check a renderer out of the pool and use it to render the component.
17+
# @return [String] Prerendered HTML from `component_name`
1518
def self.render(component_name, props, prerender_options)
1619
@@pool.with do |renderer|
1720
renderer.render(component_name, props, prerender_options)
1821
end
1922
end
2023

21-
def self.create_renderer
22-
renderer.new(renderer_options)
24+
def self.checkout_renderer
25+
@@pool.checkout
26+
end
27+
28+
def self.checkin_renderer(renderer)
29+
@@pool.checkin
2330
end
2431

2532
class PrerenderError < RuntimeError
@@ -30,4 +37,4 @@ def initialize(component_name, props, js_message)
3037
end
3138
end
3239
end
33-
end
40+
end

lib/react/server_rendering/exec_js_renderer.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ module ServerRendering
55
# - No Rails dependency
66
# - No browser concerns
77
class ExecJSRenderer
8+
# @return [ExecJS::Runtime::Context] The JS context for this renderer
9+
attr_reader :context
10+
811
def initialize(options={})
912
js_code = options[:code] || raise("Pass `code:` option to instantiate a JS context!")
1013
@context = ExecJS.compile(GLOBAL_WRAPPER + js_code)

test/dummy/app/controllers/server_controller.rb

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,6 @@ def show
44
end
55

66
def console_example
7-
React::ServerRendering.renderer_options = {replay_console: true}
8-
React::ServerRendering.reset_pool
9-
@todos = %w{todo1 todo2 todo3}
10-
end
11-
12-
def console_example_suppressed
13-
React::ServerRendering.renderer_options = {replay_console: false}
14-
React::ServerRendering.reset_pool
157
@todos = %w{todo1 todo2 todo3}
168
end
179

test/dummy/config/routes.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
resources :server, only: [:show] do
44
collection do
55
get :console_example
6-
get :console_example_suppressed
76
get :inline_component
87
end
98
end

test/react/rails/component_mount_test.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,23 @@ class ComponentMountTest < ActionDispatch::IntegrationTest
7272
assert html.include?('class="test"')
7373
assert html.include?('data-foo="1"')
7474
end
75+
76+
module DummyRenderer
77+
def self.render(component_name, props, prerender_options)
78+
"rendered #{component_name} with #{props.to_json}"
79+
end
80+
end
81+
82+
module DummyController
83+
def self.__prerenderer
84+
DummyRenderer
85+
end
86+
end
87+
88+
test "it uses the controller's __prerenderer, if available" do
89+
@helper.setup(DummyController)
90+
rendered_component = @helper.react_component('Foo', {"ok" => true}, prerender: :static)
91+
assert_equal %|<div>rendered Foo with {&quot;ok&quot;:true}</div>|, rendered_component
92+
end
7593
end
7694
end

test/react/server_rendering_test.rb

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,6 @@ class ReactServerRenderingTest < ActiveSupport::TestCase
2626
React::ServerRendering.reset_pool
2727
end
2828

29-
test '.create_renderer makes a renderer with initialization options' do
30-
mock_renderer = Minitest::Mock.new
31-
mock_renderer.expect(:new, :fake_renderer, [{mock: true}])
32-
React::ServerRendering.renderer = mock_renderer
33-
React::ServerRendering.renderer_options = {mock: true}
34-
renderer = React::ServerRendering.create_renderer
35-
assert_equal(:fake_renderer, renderer)
36-
end
37-
3829
test '.render returns a rendered string' do
3930
props = {"props" => true}
4031
result = React::ServerRendering.render("MyComponent", props, "prerender-opts")
@@ -53,4 +44,4 @@ class ReactServerRenderingTest < ActiveSupport::TestCase
5344
assert_match(/^DIFFERENT/, React::ServerRendering.render(nil, nil, nil))
5445
assert_match(/^DIFFERENT/, React::ServerRendering.render(nil, nil, nil))
5546
end
56-
end
47+
end

test/server_rendered_html_test.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,19 @@ def wait_to_ensure_asset_pipeline_detects_changes
3939

4040
test 'react server rendering shows console output as html comment' do
4141
# Make sure console messages are replayed when requested
42+
React::ServerRendering.renderer_options = {replay_console: true}
43+
React::ServerRendering.reset_pool
4244
get '/server/console_example'
4345
assert_match(/Console Logged/, response.body)
4446
assert_match(/console.log.apply\(console, \["got initial state"\]\)/, response.body)
4547
assert_match(/console.warn.apply\(console, \["mounted component"\]\)/, response.body)
4648
assert_match(/console.error.apply\(console, \["rendered!","foo"\]\)/, response.body)
4749

4850
# Make sure they're not when we don't ask for them
49-
get '/server/console_example_suppressed'
51+
React::ServerRendering.renderer_options = {replay_console: false}
52+
React::ServerRendering.reset_pool
53+
54+
get '/server/console_example'
5055
assert_match(/Console Logged/, response.body)
5156
assert_no_match(/console.log/, response.body)
5257
assert_no_match(/console.warn/, response.body)

0 commit comments

Comments
 (0)