Skip to content

Decouple renderer #253

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

Merged
merged 9 commits into from
May 15, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Guardfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
guard :minitest do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guard throws a circular loading in progress warning

# with Minitest::Unit
watch(%r{^test/(.*)\/?(.*)_test\.rb$})
watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
watch(%r{^test/test_helper\.rb$}) { 'test' }
end
20 changes: 8 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,18 +154,14 @@ You can configure your pool of JS virtual machines and specify where it should l
# config/environments/application.rb
# These are the defaults if you dont specify any yourself
MyApp::Application.configure do
# renderer pool size:
config.react.max_renderers = 10
# prerender timeout, in seconds:
config.react.timeout = 20
# where to get React.js source:
config.react.react_js = lambda { File.read(::Rails.application.assets.resolve('react.js')) }
# array of filenames that will be requested from the asset pipeline
# and concatenated:
config.react.component_filenames = ['components.js']
# server-side console.log, console.warn, and console.error messages will be replayed on the client
# (you can set this to `true` in config/enviroments/development.rb to replay in development only)
config.react.replay_console = false
# Settings for the pool of renderers:
config.react.server_renderer_pool_size ||= 10
config.react.server_renderer_timeout ||= 20 # seconds
config.react.server_renderer = React::ServerRendering::SprocketsRenderer
config.react.server_renderer_options = {
files: ["react.js", "components.js"], # files to load for prerendering
replay_console: true, # if true, console.* will be replayed client-side
}
end
```

Expand Down
3 changes: 1 addition & 2 deletions lib/react-rails.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
require 'react/jsx'
require 'react/renderer'
require 'react/rails'
require 'react/console'
require 'react/server_rendering'

30 changes: 0 additions & 30 deletions lib/react/console.rb

This file was deleted.

35 changes: 14 additions & 21 deletions lib/react/rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ class Railtie < ::Rails::Railtie
config.react.variant = (::Rails.env.production? ? :production : :development)
config.react.addons = false
config.react.jsx_transform_options = {}
# Server-side rendering
config.react.max_renderers = 10
config.react.timeout = 20 #seconds
config.react.react_js = lambda {File.read(::Rails.application.assets.resolve('react.js'))}
config.react.component_filenames = ['components.js']
# Server rendering:
config.react.server_renderer_pool_size = 10
config.react.server_renderer_timeout = 20 # seconds
config.react.server_renderer = nil # defaults to SprocketsRenderer
config.react.server_renderer_options = {} # SprocketsRenderer provides defaults

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

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

do_setup = lambda do
cfg = app.config.react
React::Renderer.setup!( cfg.react_js, cfg.components_js, cfg.replay_console,
{:size => cfg.max_renderers, :timeout => cfg.timeout})
end

do_setup.call
React::ServerRendering.pool_size = app.config.react.server_renderer_pool_size
React::ServerRendering.pool_timeout = app.config.react.server_renderer_timeout
React::ServerRendering.renderer_options = app.config.react.server_renderer_options
React::ServerRendering.renderer = app.config.react.server_renderer

# Reload the JS VMs in dev when files change
ActionDispatch::Reloader.to_prepare(&do_setup)
React::ServerRendering.reset_pool
# Reload renderers in dev when files change
ActionDispatch::Reloader.to_prepare { React::ServerRendering.reset_pool }
end
end
end
Expand Down
17 changes: 9 additions & 8 deletions lib/react/rails/view_helper.rb
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
module React
module Rails
module ViewHelper

# Render a UJS-type HTML tag annotated with data attributes, which
# are used by react_ujs to actually instantiate the React component
# on the client.
#
def react_component(name, args = {}, options = {}, &block)
def react_component(name, props = {}, options = {}, &block)
options = {:tag => options} if options.is_a?(Symbol)
block = Proc.new{concat React::Renderer.render(name, args)} if options[:prerender]

prerender_options = options[:prerender]
if prerender_options
block = Proc.new{ concat React::ServerRendering.render(name, props.merge(prerender: prerender_options)) }
end

html_options = options.reverse_merge(:data => {})
html_options[:data].tap do |data|
data[:react_class] = name
data[:react_props] = React::Renderer.react_props(args) unless args.empty?
data[:react_props] = (props.is_a?(String) ? props : props.to_json)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should always be JSON because ReactRailsUJS will try to parse it as JSON!

end
html_tag = html_options[:tag] || :div

# remove internally used properties so they aren't rendered to DOM
html_options.except!(:tag, :prerender)

content_tag(html_tag, '', html_options, &block)
end

end
end
end
83 changes: 0 additions & 83 deletions lib/react/renderer.rb

This file was deleted.

24 changes: 24 additions & 0 deletions lib/react/server_rendering.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require 'connection_pool'
require 'react/server_rendering/sprockets_renderer'

module React
module ServerRendering
mattr_accessor :renderer, :renderer_options,
:pool_size, :pool_timeout

def self.reset_pool
options = {size: pool_size, timeout: pool_timeout}
@@pool = ConnectionPool.new(options) { create_renderer }
end

def self.render(component_name, props)
@@pool.with do |renderer|
renderer.render(component_name, props)
end
end

def self.create_renderer
renderer.new(renderer_options)
end
end
end
81 changes: 81 additions & 0 deletions lib/react/server_rendering/sprockets_renderer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
module React
module ServerRendering
class SprocketsRenderer
def initialize(options={})
@replay_console = options.fetch(:replay_console, true)

filenames = options.fetch(:files, ["react.js", "components.js"])
js_code = GLOBAL_WRAPPER + CONSOLE_POLYFILL

filenames.each do |filename|
js_code << ::Rails.application.assets[filename].to_s
end

@context = ExecJS.compile(js_code)
end

def render(component_name, props)
# pass prerender: :static to use renderToStaticMarkup
if props.is_a?(Hash) && props[:prerender] == :static
react_render_method = "renderToStaticMarkup"
else
react_render_method = "renderToString"
end

if !props.is_a?(String)
props = props.to_json
end

js_code = <<-JS
(function () {
var result = React.#{react_render_method}(React.createElement(#{component_name}, #{props}));
#{@replay_console ? CONSOLE_REPLAY : ""}
return result;
})()
JS

@context.eval(js_code).html_safe
rescue ExecJS::ProgramError => err
raise PrerenderError.new(component_name, props, err)
end

# Handle node.js & other RubyRacer contexts
GLOBAL_WRAPPER = <<-JS
var global = global || this;
var self = self || this;
var window = window || this;
JS

# Reimplement console methods for replaying on the client
CONSOLE_POLYFILL = <<-JS
var console = { history: [] };
['error', 'log', 'info', 'warn'].forEach(function (fn) {
console[fn] = function () {
console.history.push({level: fn, arguments: Array.prototype.slice.call(arguments)});
};
});
JS

# Replay message from console history
CONSOLE_REPLAY = <<-JS
(function (history) {
if (history && history.length > 0) {
result += '\\n<scr'+'ipt>';
history.forEach(function (msg) {
result += '\\nconsole.' + msg.level + '.apply(console, ' + JSON.stringify(msg.arguments) + ');';
});
result += '\\n</scr'+'ipt>';
}
})(console.history);
JS

class PrerenderError < RuntimeError
def initialize(component_name, props, js_message)
message = ["Encountered error \"#{js_message}\" when prerendering #{component_name} with #{props}",
js_message.backtrace.join("\n")].join("\n")
super(message)
end
end
end
end
end
2 changes: 2 additions & 0 deletions react-rails.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Gem::Specification.new do |s|
s.add_development_dependency 'bundler', '>= 1.2.2'
s.add_development_dependency 'coffee-rails'
s.add_development_dependency 'es5-shim-rails', '>= 2.0.5'
s.add_development_dependency 'guard'
s.add_development_dependency 'guard-minitest'
s.add_development_dependency 'jbuilder'
s.add_development_dependency 'poltergeist', '>= 0.3.3'
s.add_development_dependency 'test-unit', '~> 2.5'
Expand Down
15 changes: 4 additions & 11 deletions test/dummy/app/controllers/server_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,14 @@ def show
end

def console_example
hack_replay_console_config true
React::ServerRendering.renderer_options = {replay_console: true}
React::ServerRendering.reset_pool
@todos = %w{todo1 todo2 todo3}
end

def console_example_suppressed
hack_replay_console_config false
React::ServerRendering.renderer_options = {replay_console: false}
React::ServerRendering.reset_pool
@todos = %w{todo1 todo2 todo3}
end

private
def hack_replay_console_config(value)
# Don't do this in your app; just set it how you want it in config/application.rb
cfg = ::Rails.application.config.react
cfg.replay_console = value
React::Renderer.setup!( cfg.react_js, cfg.components_js, cfg.replay_console,
{:size => cfg.max_renderers, :timeout => cfg.timeout})
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@ class ViewHelperTest < ActionDispatch::IntegrationTest
test 'react server rendering also gets mounted on client' do
visit '/server/1'
assert_match(/data-react-class=\"TodoList\"/, page.html)
assert_match(/data-react-checksum/, page.html)
assert_match(/yep/, page.find("#status").text)
end

Expand Down
Loading