Skip to content

Commit 51ec36c

Browse files
committed
feat(ServerRendering) watch app/javascripts; improve UJS for server rendering
1 parent 15d5d2d commit 51ec36c

File tree

18 files changed

+172
-38
lines changed

18 files changed

+172
-38
lines changed

lib/assets/javascripts/react_ujs.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,15 +125,16 @@ module.exports = function(ujs) {
125125
// Assume className is simple and can be found at top-level (window).
126126
// Fallback to eval to handle cases like 'My.React.ComponentName'.
127127
// Also, try to gracefully import Babel 6 style default exports
128+
var topLevel = typeof window === "undefined" ? this : window;
129+
128130
module.exports = function(className) {
129131
var constructor;
130-
var topLevel = typeof window === "undefined" ? this : window;
131132
// Try to access the class globally first
132133
constructor = topLevel[className];
133134

134135
// If that didn't work, try eval
135136
if (!constructor) {
136-
constructor = eval.call(topLevel, className);
137+
constructor = eval(className);
137138
}
138139

139140
// Lastly, if there is a default attribute try that
@@ -308,6 +309,12 @@ if (typeof window !== "undefined") {
308309
detectEvents(ReactRailsUJS)
309310
}
310311

312+
// It's a bit of a no-no to populate the global namespace,
313+
// but we really need it!
314+
// We need access to this object for server rendering, and
315+
// we can't do a dynamic `require`, so we'll grab it from here:
316+
this.ReactRailsUJS = ReactRailsUJS
317+
311318
module.exports = ReactRailsUJS
312319

313320

lib/react/rails/railtie.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ class Railtie < ::Rails::Railtie
2020
# Changing files with these extensions in these directories will cause the server renderer to reload:
2121
config.react.server_renderer_directories = ["/app/assets/javascripts/"]
2222
config.react.server_renderer_extensions = ["jsx"]
23+
if defined?(Webpacker)
24+
config.react.server_renderer_directories << "app/javascript"
25+
config.react.server_renderer_extensions << "js"
26+
end
2327
# View helper implementation:
2428
config.react.view_helper_implementation = nil # Defaults to ComponentMount
2529

@@ -30,6 +34,7 @@ class Railtie < ::Rails::Railtie
3034
memo[app_dir] = config.react.server_renderer_extensions
3135
memo
3236
end
37+
3338
app.reloaders << ActiveSupport::FileUpdateChecker.new([], reload_paths) do
3439
React::ServerRendering.reset_pool
3540
end

lib/react/server_rendering/exec_js_renderer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def render_from_parts(before, main, after)
4141

4242
def main_render(component_name, props, prerender_options)
4343
render_function = prerender_options.fetch(:render_function, "renderToString")
44-
"ReactRailsUJS.serverRender('#{render_function}', #{component_name}, #{props})"
44+
"this.ReactRailsUJS.serverRender('#{render_function}', '#{component_name}', #{props})"
4545
end
4646

4747
def compose_js(before, main, after)

react_ujs/dist/react_ujs.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,15 +125,16 @@ module.exports = function(ujs) {
125125
// Assume className is simple and can be found at top-level (window).
126126
// Fallback to eval to handle cases like 'My.React.ComponentName'.
127127
// Also, try to gracefully import Babel 6 style default exports
128+
var topLevel = typeof window === "undefined" ? this : window;
129+
128130
module.exports = function(className) {
129131
var constructor;
130-
var topLevel = typeof window === "undefined" ? this : window;
131132
// Try to access the class globally first
132133
constructor = topLevel[className];
133134

134135
// If that didn't work, try eval
135136
if (!constructor) {
136-
constructor = eval.call(topLevel, className);
137+
constructor = eval(className);
137138
}
138139

139140
// Lastly, if there is a default attribute try that
@@ -308,6 +309,12 @@ if (typeof window !== "undefined") {
308309
detectEvents(ReactRailsUJS)
309310
}
310311

312+
// It's a bit of a no-no to populate the global namespace,
313+
// but we really need it!
314+
// We need access to this object for server rendering, and
315+
// we can't do a dynamic `require`, so we'll grab it from here:
316+
this.ReactRailsUJS = ReactRailsUJS
317+
311318
module.exports = ReactRailsUJS
312319

313320

react_ujs/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,10 @@ if (typeof window !== "undefined") {
109109
detectEvents(ReactRailsUJS)
110110
}
111111

112+
// It's a bit of a no-no to populate the global namespace,
113+
// but we really need it!
114+
// We need access to this object for server rendering, and
115+
// we can't do a dynamic `require`, so we'll grab it from here:
116+
this.ReactRailsUJS = ReactRailsUJS
117+
112118
module.exports = ReactRailsUJS

react_ujs/src/getConstructor/fromGlobal.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
// Assume className is simple and can be found at top-level (window).
22
// Fallback to eval to handle cases like 'My.React.ComponentName'.
33
// Also, try to gracefully import Babel 6 style default exports
4+
var topLevel = typeof window === "undefined" ? this : window;
5+
46
module.exports = function(className) {
57
var constructor;
6-
var topLevel = typeof window === "undefined" ? this : window;
78
// Try to access the class globally first
89
constructor = topLevel[className];
910

1011
// If that didn't work, try eval
1112
if (!constructor) {
12-
constructor = eval.call(topLevel, className);
13+
constructor = eval(className);
1314
}
1415

1516
// Lastly, if there is a default attribute try that

react_ujs/yarn.lock

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -993,8 +993,8 @@ [email protected]:
993993
resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765"
994994

995995
nan@^2.3.0:
996-
version "2.5.1"
997-
resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2"
996+
version "2.6.1"
997+
resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.1.tgz#8c84f7b14c96b89f57fbc838012180ec8ca39a01"
998998

999999
node-libs-browser@^2.0.0:
10001000
version "2.0.0"
@@ -1252,8 +1252,8 @@ randombytes@^2.0.0, randombytes@^2.0.1:
12521252
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.3.tgz#674c99760901c3c4112771a31e521dc349cc09ec"
12531253

12541254
rc@^1.1.7:
1255-
version "1.2.0"
1256-
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.0.tgz#c7de973b7b46297c041366b2fd3d2363b1697c66"
1255+
version "1.2.1"
1256+
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
12571257
dependencies:
12581258
deep-extend "~0.4.0"
12591259
ini "~1.3.0"
@@ -1275,7 +1275,7 @@ read-pkg@^1.0.0:
12751275
normalize-package-data "^2.3.2"
12761276
path-type "^1.0.0"
12771277

1278-
"readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.0, readable-stream@^2.1.4:
1278+
"readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.4, readable-stream@^2.2.6:
12791279
version "2.2.6"
12801280
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.6.tgz#8b43aed76e71483938d12a8d46c6cf1a00b1f816"
12811281
dependencies:
@@ -1447,12 +1447,12 @@ stream-browserify@^2.0.1:
14471447
readable-stream "^2.0.2"
14481448

14491449
stream-http@^2.3.1:
1450-
version "2.6.3"
1451-
resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.6.3.tgz#4c3ddbf9635968ea2cfd4e48d43de5def2625ac3"
1450+
version "2.7.0"
1451+
resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.0.tgz#cec1f4e3b494bc4a81b451808970f8b20b4ed5f6"
14521452
dependencies:
14531453
builtin-status-codes "^3.0.0"
14541454
inherits "^2.0.1"
1455-
readable-stream "^2.1.0"
1455+
readable-stream "^2.2.6"
14561456
to-arraybuffer "^1.0.0"
14571457
xtend "^4.0.0"
14581458

test/dummy/app/controllers/server_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
class ServerController < ApplicationController
22
def show
3+
@component_name = params[:component_name] || "TodoList"
34
@todos = %w{todo1 todo2 todo3}
45
end
56

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
var React = require("react")
2+
3+
module.exports = React.createClass({
4+
getInitialState: function() {
5+
return({mounted: "nope"});
6+
},
7+
componentWillMount: function() {
8+
this.setState({mounted: 'yep'});
9+
},
10+
render: function() {
11+
return (
12+
<ul>
13+
<li id='status'>{this.state.mounted}</li>
14+
{this.props.todos.map(function(todo, i) {
15+
return (<li key={i}>{todo}</li>)
16+
})}
17+
<li>From Webpacker</li>
18+
</ul>
19+
)
20+
}
21+
})
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
var React = require("react")
2+
3+
module.exports = React.createClass({
4+
getInitialState: function() {
5+
console.log('got initial state');
6+
return({mounted: "nope"});
7+
},
8+
componentWillMount: function() {
9+
console.warn('mounted component');
10+
this.setState({mounted: 'yep'});
11+
},
12+
render: function() {
13+
var x = 'foo';
14+
console.error('rendered!', x);
15+
return (
16+
<ul>
17+
<li>Console Logged</li>
18+
<li id='status'>{this.state.mounted}</li>
19+
{this.props.todos.map(function(todo, i) {
20+
return (<li key={i}>{todo}</li>)
21+
})}
22+
</ul>
23+
)
24+
}
25+
})
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
var React = require("react")
2+
module.exports = React.createClass({
3+
componentWillMount: function () {
4+
setTimeout(function () {}, 1000)
5+
clearTimeout(0)
6+
},
7+
render: function () {
8+
return <span>I am rendered!</span>
9+
}
10+
})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// By default, this pack is loaded for server-side rendering.
2+
// It must expose react_ujs as `ReactRailsUJS` and prepare a require context.
3+
var componentRequireContext = require.context("components", true)
4+
var ReactRailsUJS = require("react_ujs")
5+
ReactRailsUJS.loadContext(componentRequireContext)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<%= react_component "TodoList", {todos: @todos}, {prerender: true} %>
1+
<%= react_component @component_name, {todos: @todos}, {prerender: true} %>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
var React = require("react")
2+
module.exports = React.createClass({
3+
render: function() {
4+
return (
5+
<ul>
6+
<li>Updated</li>
7+
</ul>
8+
)
9+
}
10+
})

test/react/server_rendering/exec_js_renderer_test.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
var React = {
66
createElement: function() {},
77
}
8-
var ReactRailsUJS = {
8+
this.ReactRailsUJS = {
99
serverRender: function() {
1010
return 'serverRender was called'
1111
},
@@ -50,7 +50,6 @@ def @renderer.after_render(name, props, opts)
5050
assert_no_match(/assigned_after_render/, error.message)
5151
end
5252

53-
5453
test '#after_render is called after #before_render' do
5554
def @renderer.before_render(name, props, opts)
5655
"var beforeRenderVar = 'assigned_before_render'"

test/react/server_rendering/sprockets_renderer_test.rb

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,15 @@ class SprocketsRendererTest < ActiveSupport::TestCase
7777
assert_match(/console.error.apply\(console, \["setTimeout #{message}"\]\);$/, result)
7878
end
7979

80-
test '.new accepts additional code to add to the JS context' do
81-
additional_code = File.read(File.expand_path("../../../helper_files/WithoutSprockets.js", __FILE__))
80+
if !WebpackerHelpers.available?
81+
# This doesn't work with webpacker since finding components is based on filename
82+
test '.new accepts additional code to add to the JS context' do
83+
additional_code = File.read(File.expand_path("../../../helper_files/WithoutSprockets.js", __FILE__))
8284

83-
additional_renderer = React::ServerRendering::SprocketsRenderer.new(code: additional_code)
85+
additional_renderer = React::ServerRendering::SprocketsRenderer.new(code: additional_code)
8486

85-
assert_match(/drink more caffeine<\/span>/, additional_renderer.render("WithoutSprockets", {label: "drink more caffeine"}, nil))
87+
assert_match(/drink more caffeine<\/span>/, additional_renderer.render("WithoutSprockets", {label: "drink more caffeine"}, nil))
88+
end
8689
end
8790

8891
test '.new accepts any filenames' do

test/server_rendered_html_test.rb

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,22 @@ def wait_to_ensure_asset_pipeline_detects_changes
1010
sleep(1)
1111
end
1212

13+
setup do
14+
if WebpackerHelpers.available?
15+
WebpackerHelpers.compile
16+
end
17+
end
18+
1319
test 'react server rendering reloads jsx after changes to the jsx files' do
14-
file_with_updates = File.expand_path('../helper_files/TodoListWithUpdates.js.jsx', __FILE__)
15-
file_without_updates = File.expand_path('../helper_files/TodoListWithoutUpdates.js.jsx', __FILE__)
16-
app_file = File.expand_path('../dummy/app/assets/javascripts/components/TodoList.js.jsx', __FILE__)
20+
if WebpackerHelpers.available?
21+
file_with_updates = File.expand_path('../helper_files/TodoListWithUpdates.js', __FILE__)
22+
file_without_updates = File.expand_path('../helper_files/TodoListWithoutUpdates.js', __FILE__)
23+
app_file = File.expand_path('../dummy/app/javascript/components/TodoList.js', __FILE__)
24+
else
25+
file_with_updates = File.expand_path('../helper_files/TodoListWithUpdates.js.jsx', __FILE__)
26+
file_without_updates = File.expand_path('../helper_files/TodoListWithoutUpdates.js.jsx', __FILE__)
27+
app_file = File.expand_path('../dummy/app/assets/javascripts/components/TodoList.js.jsx', __FILE__)
28+
end
1729

1830
FileUtils.cp app_file, file_without_updates
1931
FileUtils.touch app_file
@@ -25,7 +37,11 @@ def wait_to_ensure_asset_pipeline_detects_changes
2537

2638
FileUtils.cp file_with_updates, app_file
2739
FileUtils.touch app_file
28-
wait_to_ensure_asset_pipeline_detects_changes
40+
if WebpackerHelpers.available?
41+
WebpackerHelpers.compile
42+
else
43+
wait_to_ensure_asset_pipeline_detects_changes
44+
end
2945

3046
get '/server/1'
3147
assert_match(/Updated/, response.body)
@@ -37,21 +53,36 @@ def wait_to_ensure_asset_pipeline_detects_changes
3753
end
3854
end
3955

40-
test 'it reloads when new jsx files are added' do
56+
test 'it reloads when new jsx files are added to the asset pipeline' do
4157
begin
42-
get '/server/1'
43-
refute_match(/Overwritten List/, response.body)
58+
assert_raises(ActionView::Template::Error) {
59+
get '/server/1?component_name=NewList'
60+
}
4461

45-
# Make it alphabetically last so it will override the preceeding one:
46-
new_file_path = File.expand_path('../dummy/app/assets/javascripts/components/ZZ_NewComponent.js.jsx', __FILE__)
47-
File.write new_file_path, <<-JS
48-
var TodoList = function() { return <span>"Overwritten List"</span> }
49-
JS
62+
if WebpackerHelpers.available?
63+
new_file_path = '../dummy/app/javascript/components/NewList.js'
64+
new_file_contents = <<-JS
65+
var React = require("react")
66+
module.exports = function() { return <span>"New List"</span> }
67+
JS
68+
else
69+
new_file_path = '../dummy/app/assets/javascripts/components/ZZ_NewComponent.js.jsx'
70+
new_file_contents = <<-JS
71+
var NewList = function() { return <span>"New List"</span> }
72+
JS
73+
end
5074

51-
wait_to_ensure_asset_pipeline_detects_changes
75+
new_file_path = File.expand_path(new_file_path, __FILE__)
76+
File.write new_file_path, new_file_contents
5277

53-
get '/server/1'
54-
assert_match(/Overwritten List/, response.body)
78+
if WebpackerHelpers.available?
79+
WebpackerHelpers.compile
80+
else
81+
wait_to_ensure_asset_pipeline_detects_changes
82+
end
83+
84+
get '/server/1?component_name=NewList'
85+
assert_match(/New List/, response.body)
5586
ensure
5687
FileUtils.rm_rf(new_file_path)
5788
wait_to_ensure_asset_pipeline_detects_changes

test/support/webpacker_helpers.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ def when_webpacker_available
1313
def compile
1414
clear_webpacker_packs
1515
Dir.chdir("./test/dummy") do
16-
Rake::Task['webpacker:compile'].invoke
16+
capture_io do
17+
Rake::Task['webpacker:compile'].reenable
18+
Rake::Task['webpacker:compile'].invoke
19+
end
1720
end
1821
# Reload cached JSON manifest:
1922
Webpacker::Manifest.load

0 commit comments

Comments
 (0)