Skip to content

Commit 79101ff

Browse files
authored
Merge pull request #559 from reactjs/per-request-js-ctx
feat(ServerRendering) support per-request renderers
2 parents 865f80e + f654c33 commit 79101ff

19 files changed

+145
-36
lines changed

Appraisals

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,25 @@ appraise "rails-4.2-sprockets_4" do
4444
gem "sprockets", "~> 4.0.x"
4545
gem "turbolinks", "~> 2.5.0"
4646
gem "webpacker", github: "rails/webpacker"
47+
# This ExecJS backend provides stateful context
48+
# which the default nodejs backend does not
49+
gem "mini_racer"
4750
end
4851

4952
appraise "rails-5" do
5053
gem 'rails', '~> 5.0.0'
5154
gem "turbolinks", "~> 5.0.0"
5255
gem "webpacker", github: "rails/webpacker"
56+
# This ExecJS backend provides stateful context
57+
# which the default nodejs backend does not
58+
gem "therubyracer"
5359
end
5460

5561
appraise "rails-5-no_sprockets" do
62+
# Appraisal adds `turbolinks` to this gemfile because it is
63+
# present in `./Gemfile`.
64+
# But it causes this gemfile to break, so it must be removed
65+
# from `./gemfiles/rails_5_no_sprockets.gemfile` manually.
5666
gem 'rails', '~> 5.0.0'
5767
end
5868

gemfiles/rails_4.2_sprockets_4.gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ gem "rails", "~> 4.2.1"
66
gem "sprockets", "~> 4.0.x"
77
gem "turbolinks", "~> 2.5.0"
88
gem "webpacker", :github => "rails/webpacker"
9+
gem "mini_racer"
910

1011
gemspec :path => "../"

gemfiles/rails_5.gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ source "http://rubygems.org"
55
gem "rails", "~> 5.0.0"
66
gem "turbolinks", "~> 5.0.0"
77
gem "webpacker", :github => "rails/webpacker"
8+
gem "therubyracer"
89

910
gemspec :path => "../"

gemfiles/rails_5_no_sprockets.gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# This file was generated by Appraisal
2+
# This shouldn't have turbolinks or sprockets
23

34
source "http://rubygems.org"
45

lib/react/rails/component_mount.rb

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ 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)
2021
end
2122

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

3132
prerender_options = options[:prerender]
3233
if prerender_options
33-
block = Proc.new{ concat React::ServerRendering.render(name, props, prerender_options) }
34+
block = Proc.new{ concat(prerender_component(name, props, prerender_options)) }
3435
end
3536

3637
html_options = options.reverse_merge(:data => {})
@@ -47,6 +48,15 @@ def react_component(name, props = {}, options = {}, &block)
4748

4849
content_tag(html_tag, '', html_options, &block)
4950
end
51+
52+
private
53+
54+
# If this controller has checked out a renderer, use that one.
55+
# Otherwise, use {React::ServerRendering} directly (which will check one out for this rendering).
56+
def prerender_component(component_name, props, prerender_options)
57+
renderer = @controller.try(:react_rails_prerenderer) || React::ServerRendering
58+
renderer.render(component_name, props, prerender_options)
59+
end
5060
end
5161
end
5262
end

lib/react/rails/controller_lifecycle.rb

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,53 @@
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

68
included do
79
# use both names to support Rails 3..5
8-
before_action_with_fallback = respond_to?(:before_action) ? :before_action : :before_filter
9-
after_action_with_fallback = respond_to?(:after_action) ? :after_action : :after_filter
10-
public_send(before_action_with_fallback, :setup_react_component_helper)
11-
public_send(after_action_with_fallback, :teardown_react_component_helper)
10+
around_action_with_fallback = respond_to?(:around_action) ? :around_action : :around_filter
11+
public_send(around_action_with_fallback, :use_react_component_helper)
1212
attr_reader :__react_component_helper
1313
end
1414

15-
def setup_react_component_helper
15+
module ClassMethods
16+
# Call this in the controller to check out a prerender for the whole request.
17+
# You can access the renderer with {#react_rails_prerenderer}.
18+
def per_request_react_rails_prerenderer
19+
around_action_with_fallback = respond_to?(:around_action) ? :around_action : :around_filter
20+
public_send(around_action_with_fallback, :per_request_react_rails_prerenderer)
21+
end
22+
end
23+
24+
# Instantiate the ViewHelper implementation and call its #setup method
25+
# then let the controller action run,
26+
# then call the ViewHelper implementation's #teardown method
27+
def use_react_component_helper
1628
new_helper = React::Rails::ViewHelper.helper_implementation_class.new
1729
new_helper.setup(self)
1830
@__react_component_helper = new_helper
31+
yield
32+
@__react_component_helper.teardown(self)
1933
end
2034

21-
def teardown_react_component_helper
22-
@__react_component_helper.teardown(self)
35+
# If you want a per-request renderer, add this method as an around-action
36+
#
37+
# (`.per_request_react_rails_prerenderer` does this for you)
38+
# @example Having one renderer instance for each controller action
39+
# around_action :per_request_react_rails_prerenderer
40+
def per_request_react_rails_prerenderer
41+
React::ServerRendering.with_renderer do |renderer|
42+
@__react_rails_prerenderer = renderer
43+
yield
44+
end
45+
end
46+
47+
48+
# An instance of a server renderer, for use during this request
49+
def react_rails_prerenderer
50+
@__react_rails_prerenderer
2351
end
2452
end
2553
end

lib/react/server_rendering.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,23 @@ 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+
# Yield a renderer for an arbitrary block
25+
def self.with_renderer
26+
@@pool.with { |renderer| yield(renderer) }
2327
end
2428

2529
class PrerenderError < RuntimeError

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)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
//= require_self
22
//= require_tree ./components
3+
//= require ./pages

test/dummy/app/assets/javascripts/pages.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
var GreetingMessage = React.createClass({
22
getInitialState: function() {
3-
return {greeting: 'Hello'};
3+
var initialGreeting = 'Hello';
4+
if (typeof global !== "undefined" && global.ctx && global.ctx.greeting) {
5+
initialGreeting = global.ctx.greeting
6+
}
7+
8+
return {
9+
greeting: initialGreeting
10+
}
411
},
512
goodbye: function() {
613
this.setState({greeting: 'Goodbye'});

0 commit comments

Comments
 (0)