Skip to content

Commit fbe18b8

Browse files
authored
Merge pull request #683 from reactjs/webpacker-ssr
Webpacker Server Rendering
2 parents d3d82a0 + 339799a commit fbe18b8

30 files changed

+402
-90
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)

lib/react/server_rendering/sprockets_renderer.rb

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require "react/server_rendering/environment_container"
22
require "react/server_rendering/manifest_container"
3+
require "react/server_rendering/webpacker_manifest_container"
34
require "react/server_rendering/yaml_manifest_container"
45

56
module React
@@ -57,19 +58,7 @@ class << self
5758
#
5859
# @return [#find_asset(logical_path)] An object that returns asset contents by logical path
5960
def asset_container
60-
@asset_container ||= if self.class.asset_container_class.present?
61-
self.class.asset_container_class.new
62-
elsif assets_precompiled? && ManifestContainer.compatible?
63-
ManifestContainer.new
64-
elsif assets_precompiled? && YamlManifestContainer.compatible?
65-
YamlManifestContainer.new
66-
else
67-
EnvironmentContainer.new
68-
end
69-
end
70-
71-
def assets_precompiled?
72-
!::Rails.application.config.assets.compile
61+
@asset_container ||= asset_container_class.new
7362
end
7463

7564
private
@@ -97,6 +86,29 @@ def render_function(opts)
9786
def prepare_props(props)
9887
props.is_a?(String) ? props : props.to_json
9988
end
89+
90+
def assets_precompiled?
91+
!::Rails.application.config.assets.compile
92+
end
93+
94+
def asset_container_class
95+
if self.class.asset_container_class.present?
96+
self.class.asset_container_class
97+
elsif WebpackerManifestContainer.compatible?
98+
WebpackerManifestContainer
99+
elsif assets_precompiled?
100+
if ManifestContainer.compatible?
101+
ManifestContainer
102+
elsif YamlManifestContainer.compatible?
103+
YamlManifestContainer
104+
else
105+
# Even though they are precompiled, we can't find them :S
106+
EnvironmentContainer
107+
end
108+
else
109+
EnvironmentContainer
110+
end
111+
end
100112
end
101113
end
102114
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
require "open-uri"
2+
3+
module React
4+
module ServerRendering
5+
# Get a compiled file from Webpacker
6+
class WebpackerManifestContainer
7+
def find_asset(logical_path)
8+
asset_path = Webpacker::Manifest.lookup(logical_path) # raises if not found
9+
if asset_path.start_with?("http")
10+
# TODO: this includes webpack-dev-server code which causes ExecJS to 💥
11+
dev_server_asset = open(asset_path).read
12+
else
13+
full_path = File.join(
14+
# TODO: using `.parent` here won't work for nonstandard configurations
15+
Webpacker::Configuration.output_path.parent,
16+
asset_path
17+
)
18+
File.read(full_path)
19+
end
20+
end
21+
22+
def self.compatible?
23+
!!defined?(Webpacker)
24+
end
25+
end
26+
end
27+
end

lib/react/server_rendering/yaml_manifest_container.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module React
22
module ServerRendering
3-
# Get asset content by reading the compiled file from disk using the generated maniftest.yml file
3+
# Get asset content by reading the compiled file from disk using the generated manifest.yml file
44
#
55
# This is good for Rails production when assets are compiled to public/assets
66
# but sometimes, they're compiled to other directories (or other servers)

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+
self.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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
var React = require("react")
2+
3+
module.exports = React.createClass({
4+
getInitialState: function() {
5+
var initialGreeting = 'Hello';
6+
if (typeof global !== "undefined" && global.ctx && global.ctx.greeting) {
7+
initialGreeting = global.ctx.greeting
8+
}
9+
10+
return {
11+
greeting: initialGreeting
12+
}
13+
},
14+
goodbye: function() {
15+
this.setState({greeting: 'Goodbye'});
16+
},
17+
render: function() {
18+
return React.DOM.div({},
19+
React.DOM.div({}, this.state.greeting, ' ', this.props.name),
20+
React.DOM.button({onClick: this.goodbye}, 'Goodbye')
21+
);
22+
}
23+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
var React = require("react")
2+
module.exports = React.createClass({
3+
render: function() {
4+
return React.createElement("li", null, this.props.todo)
5+
}
6+
})
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/index")
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/rails/component_mount_test.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
when_sprockets_available do
44
class ComponentMountTest < ActionDispatch::IntegrationTest
55
setup do
6+
WebpackerHelpers.compile_if_missing
67
@helper = React::Rails::ComponentMount.new
78
end
89

test/react/rails/controller_lifecycle_test.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def react_component(*args)
2424

2525
class ControllerLifecycleTest < ActionDispatch::IntegrationTest
2626
def setup
27+
WebpackerHelpers.compile_if_missing
2728
@previous_helper_implementation = React::Rails::ViewHelper.helper_implementation_class
2829
React::Rails::ViewHelper.helper_implementation_class = DummyHelperImplementation
2930
end

0 commit comments

Comments
 (0)