Skip to content

Commit 7524897

Browse files
author
Robert Mosolgo
committed
Merge pull request #253 from rmosolgo/decouple-renderer
Decouple renderer
2 parents 491715c + 981e3d9 commit 7524897

18 files changed

+284
-234
lines changed

Guardfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
guard :minitest do
2+
# with Minitest::Unit
3+
watch(%r{^test/(.*)\/?(.*)_test\.rb$})
4+
watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
5+
watch(%r{^test/test_helper\.rb$}) { 'test' }
6+
end

README.md

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -154,18 +154,14 @@ You can configure your pool of JS virtual machines and specify where it should l
154154
# config/environments/application.rb
155155
# These are the defaults if you dont specify any yourself
156156
MyApp::Application.configure do
157-
# renderer pool size:
158-
config.react.max_renderers = 10
159-
# prerender timeout, in seconds:
160-
config.react.timeout = 20
161-
# where to get React.js source:
162-
config.react.react_js = lambda { File.read(::Rails.application.assets.resolve('react.js')) }
163-
# array of filenames that will be requested from the asset pipeline
164-
# and concatenated:
165-
config.react.component_filenames = ['components.js']
166-
# server-side console.log, console.warn, and console.error messages will be replayed on the client
167-
# (you can set this to `true` in config/enviroments/development.rb to replay in development only)
168-
config.react.replay_console = false
157+
# Settings for the pool of renderers:
158+
config.react.server_renderer_pool_size ||= 10
159+
config.react.server_renderer_timeout ||= 20 # seconds
160+
config.react.server_renderer = React::ServerRendering::SprocketsRenderer
161+
config.react.server_renderer_options = {
162+
files: ["react.js", "components.js"], # files to load for prerendering
163+
replay_console: true, # if true, console.* will be replayed client-side
164+
}
169165
end
170166
```
171167

lib/react-rails.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
require 'react/jsx'
2-
require 'react/renderer'
32
require 'react/rails'
4-
require 'react/console'
3+
require 'react/server_rendering'
54

lib/react/console.rb

Lines changed: 0 additions & 30 deletions
This file was deleted.

lib/react/rails/railtie.rb

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ class Railtie < ::Rails::Railtie
99
config.react.variant = (::Rails.env.production? ? :production : :development)
1010
config.react.addons = false
1111
config.react.jsx_transform_options = {}
12-
# Server-side rendering
13-
config.react.max_renderers = 10
14-
config.react.timeout = 20 #seconds
15-
config.react.react_js = lambda {File.read(::Rails.application.assets.resolve('react.js'))}
16-
config.react.component_filenames = ['components.js']
12+
# Server rendering:
13+
config.react.server_renderer_pool_size = 10
14+
config.react.server_renderer_timeout = 20 # seconds
15+
config.react.server_renderer = nil # defaults to SprocketsRenderer
16+
config.react.server_renderer_options = {} # SprocketsRenderer provides defaults
1717

1818
# Watch .jsx files for changes in dev, so we can reload the JS VMs with the new JS code.
1919
initializer "react_rails.add_watchable_files", group: :all do |app|
@@ -49,24 +49,17 @@ class Railtie < ::Rails::Railtie
4949
"react-#{variant}",
5050
].compact.join('-')
5151

52-
# Server Rendering
53-
# Concat component_filenames together for server rendering
54-
app.config.react.components_js = lambda {
55-
app.config.react.component_filenames.map do |filename|
56-
app.assets[filename].to_s
57-
end.join(";")
58-
}
52+
# The class isn't accessible in the configure block, so assign it here if it wasn't overridden:
53+
app.config.react.server_renderer ||= React::ServerRendering::SprocketsRenderer
5954

60-
do_setup = lambda do
61-
cfg = app.config.react
62-
React::Renderer.setup!( cfg.react_js, cfg.components_js, cfg.replay_console,
63-
{:size => cfg.max_renderers, :timeout => cfg.timeout})
64-
end
65-
66-
do_setup.call
55+
React::ServerRendering.pool_size = app.config.react.server_renderer_pool_size
56+
React::ServerRendering.pool_timeout = app.config.react.server_renderer_timeout
57+
React::ServerRendering.renderer_options = app.config.react.server_renderer_options
58+
React::ServerRendering.renderer = app.config.react.server_renderer
6759

68-
# Reload the JS VMs in dev when files change
69-
ActionDispatch::Reloader.to_prepare(&do_setup)
60+
React::ServerRendering.reset_pool
61+
# Reload renderers in dev when files change
62+
ActionDispatch::Reloader.to_prepare { React::ServerRendering.reset_pool }
7063
end
7164
end
7265
end

lib/react/rails/view_helper.rb

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
11
module React
22
module Rails
33
module ViewHelper
4-
54
# Render a UJS-type HTML tag annotated with data attributes, which
65
# are used by react_ujs to actually instantiate the React component
76
# on the client.
8-
#
9-
def react_component(name, args = {}, options = {}, &block)
7+
def react_component(name, props = {}, options = {}, &block)
108
options = {:tag => options} if options.is_a?(Symbol)
11-
block = Proc.new{concat React::Renderer.render(name, args)} if options[:prerender]
9+
10+
prerender_options = options[:prerender]
11+
if prerender_options
12+
block = Proc.new{ concat React::ServerRendering.render(name, props.merge(prerender: prerender_options)) }
13+
end
1214

1315
html_options = options.reverse_merge(:data => {})
1416
html_options[:data].tap do |data|
1517
data[:react_class] = name
16-
data[:react_props] = React::Renderer.react_props(args) unless args.empty?
18+
data[:react_props] = (props.is_a?(String) ? props : props.to_json)
1719
end
1820
html_tag = html_options[:tag] || :div
19-
21+
2022
# remove internally used properties so they aren't rendered to DOM
2123
html_options.except!(:tag, :prerender)
22-
24+
2325
content_tag(html_tag, '', html_options, &block)
2426
end
25-
2627
end
2728
end
2829
end

lib/react/renderer.rb

Lines changed: 0 additions & 83 deletions
This file was deleted.

lib/react/server_rendering.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
require 'connection_pool'
2+
require 'react/server_rendering/sprockets_renderer'
3+
4+
module React
5+
module ServerRendering
6+
mattr_accessor :renderer, :renderer_options,
7+
:pool_size, :pool_timeout
8+
9+
def self.reset_pool
10+
options = {size: pool_size, timeout: pool_timeout}
11+
@@pool = ConnectionPool.new(options) { create_renderer }
12+
end
13+
14+
def self.render(component_name, props)
15+
@@pool.with do |renderer|
16+
renderer.render(component_name, props)
17+
end
18+
end
19+
20+
def self.create_renderer
21+
renderer.new(renderer_options)
22+
end
23+
end
24+
end
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
module React
2+
module ServerRendering
3+
class SprocketsRenderer
4+
def initialize(options={})
5+
@replay_console = options.fetch(:replay_console, true)
6+
7+
filenames = options.fetch(:files, ["react.js", "components.js"])
8+
js_code = GLOBAL_WRAPPER + CONSOLE_POLYFILL
9+
10+
filenames.each do |filename|
11+
js_code << ::Rails.application.assets[filename].to_s
12+
end
13+
14+
@context = ExecJS.compile(js_code)
15+
end
16+
17+
def render(component_name, props)
18+
# pass prerender: :static to use renderToStaticMarkup
19+
if props.is_a?(Hash) && props[:prerender] == :static
20+
react_render_method = "renderToStaticMarkup"
21+
else
22+
react_render_method = "renderToString"
23+
end
24+
25+
if !props.is_a?(String)
26+
props = props.to_json
27+
end
28+
29+
js_code = <<-JS
30+
(function () {
31+
var result = React.#{react_render_method}(React.createElement(#{component_name}, #{props}));
32+
#{@replay_console ? CONSOLE_REPLAY : ""}
33+
return result;
34+
})()
35+
JS
36+
37+
@context.eval(js_code).html_safe
38+
rescue ExecJS::ProgramError => err
39+
raise PrerenderError.new(component_name, props, err)
40+
end
41+
42+
# Handle node.js & other RubyRacer contexts
43+
GLOBAL_WRAPPER = <<-JS
44+
var global = global || this;
45+
var self = self || this;
46+
var window = window || this;
47+
JS
48+
49+
# Reimplement console methods for replaying on the client
50+
CONSOLE_POLYFILL = <<-JS
51+
var console = { history: [] };
52+
['error', 'log', 'info', 'warn'].forEach(function (fn) {
53+
console[fn] = function () {
54+
console.history.push({level: fn, arguments: Array.prototype.slice.call(arguments)});
55+
};
56+
});
57+
JS
58+
59+
# Replay message from console history
60+
CONSOLE_REPLAY = <<-JS
61+
(function (history) {
62+
if (history && history.length > 0) {
63+
result += '\\n<scr'+'ipt>';
64+
history.forEach(function (msg) {
65+
result += '\\nconsole.' + msg.level + '.apply(console, ' + JSON.stringify(msg.arguments) + ');';
66+
});
67+
result += '\\n</scr'+'ipt>';
68+
}
69+
})(console.history);
70+
JS
71+
72+
class PrerenderError < RuntimeError
73+
def initialize(component_name, props, js_message)
74+
message = ["Encountered error \"#{js_message}\" when prerendering #{component_name} with #{props}",
75+
js_message.backtrace.join("\n")].join("\n")
76+
super(message)
77+
end
78+
end
79+
end
80+
end
81+
end

react-rails.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Gem::Specification.new do |s|
1818
s.add_development_dependency 'bundler', '>= 1.2.2'
1919
s.add_development_dependency 'coffee-rails'
2020
s.add_development_dependency 'es5-shim-rails', '>= 2.0.5'
21+
s.add_development_dependency 'guard'
22+
s.add_development_dependency 'guard-minitest'
2123
s.add_development_dependency 'jbuilder'
2224
s.add_development_dependency 'poltergeist', '>= 0.3.3'
2325
s.add_development_dependency 'test-unit', '~> 2.5'

test/dummy/app/controllers/server_controller.rb

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

66
def console_example
7-
hack_replay_console_config true
7+
React::ServerRendering.renderer_options = {replay_console: true}
8+
React::ServerRendering.reset_pool
89
@todos = %w{todo1 todo2 todo3}
910
end
1011

1112
def console_example_suppressed
12-
hack_replay_console_config false
13+
React::ServerRendering.renderer_options = {replay_console: false}
14+
React::ServerRendering.reset_pool
1315
@todos = %w{todo1 todo2 todo3}
1416
end
15-
16-
private
17-
def hack_replay_console_config(value)
18-
# Don't do this in your app; just set it how you want it in config/application.rb
19-
cfg = ::Rails.application.config.react
20-
cfg.replay_console = value
21-
React::Renderer.setup!( cfg.react_js, cfg.components_js, cfg.replay_console,
22-
{:size => cfg.max_renderers, :timeout => cfg.timeout})
23-
end
2417
end

test/view_helper_test.rb renamed to test/react/rails/view_helper_test.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,6 @@ class ViewHelperTest < ActionDispatch::IntegrationTest
129129
test 'react server rendering also gets mounted on client' do
130130
visit '/server/1'
131131
assert_match(/data-react-class=\"TodoList\"/, page.html)
132-
assert_match(/data-react-checksum/, page.html)
133132
assert_match(/yep/, page.find("#status").text)
134133
end
135134

0 commit comments

Comments
 (0)