Here is an example set up of the files:
__Procfile.dev__
web: DB_CONNECTION_POOL=${WEB_DB_CONN_POOL:-5} bundle exec puma -C config/puma.rb
worker: DB_CONNECTION_POOL=${SIDEKIQ_DB_CONN_POOL:-5} MALLOC_ARENA_MAX=2 bundle exec sidekiq -C config/sidekiq.yml -t 25
webpacker: ./bin/webpack-dev-server
__config/webpacker/yml__
development:
source_path: app/javascript
source_entry_path: packs
public_root_path: public
public_output_path: packs
cache_path: tmp/cache/webpacker
check_yarn_integrity: false
webpack_compile_output: false
dev_server:
https: false
host: localhost
port: 3035
public: localhost:3035
hmr: false
# Inline should be set to true if using HMR
inline: true
overlay: true
compress: true
disable_host_check: true
use_local_ip: false
quiet: false
headers:
'Access-Control-Allow-Origin': '*'
watch_options:
ignored: '**/node_modules/**'
__config/application.rb__
...
config.react.server_renderer_options = {
files: ["ssr_pack_one.js", "ssr_pack_two.js"], # files to load for prerendering
}
...
__packs/application.js__
import ReactRailsUJS from 'react_ujs';
import Rails from 'rails-ujs';
Rails.start();
const componentRequireContext = require.context('react/ssr_comps_one', true);
ReactRailsUJS.useContext(componentRequireContext);
__packs/ssr_pack_one.js__
import ReactRailsUJS from 'react_ujs';
const componentRequireContext = require.context('react/ssr_comps_one', true);
ReactRailsUJS.useContext(componentRequireContext);
__packs/ssr_pack_two.js__
import ReactRailsUJS from 'react_ujs';
const componentRequireContext = require.context('react/ssr_comps_two', true);
ReactRailsUJS.useContext(componentRequireContext);
__react/ssr_comps_one/TestComponent.js__
import React from 'react';
export default () => <div> Test Component</div>;
__react/ssr_comps_two/TestComponentTwo.js__
import React from 'react';
export default () => <div> Test ComponentTwo</div>;
__application.html.erb__
<!DOCTYPE html>
<html lang="en-nz">
<head>
<%= stylesheet_pack_tag "application" %>
<%= javascript_pack_tag("application", nonce: true) %>
</head>
<body>
<%= react_component("TestComponent", { }, prerender: true) %>
</body>
</html>
When using webpack dev server we should be able to specify more than one file to compile for server rendering, which can prerender a component.
We get an error from the server:
Uncaught SyntaxError: Illegal return statement at undefined:30402:10
Sprockets or Webpacker version: 4.0.1
React-Rails version: 2.4.7
React_UJS version:
Rails version: 5.2
Ruby version: 2.6.5
We want to be able to have two separate server rending packs so that we are not compiling all components when some are not needed.
During development, using the webpack-dev-server, adding a second file results in:
Uncaught SyntaxError: Illegal return statement at undefined:30402:10
Removing the file solves the issue.
It also seems to work fine when not using webpack-dev-server.
Hopefully I've provided enough information, but let me know if I can provide some more
Inline source map at the end of the packs was causing an issue with the joining of files. A new line is needed between js files so that the first line of a new file isn't on the same line as the source map, eg. in BundleRenderer:
filenames.each do |filename|
js_code << asset_container.find_asset(filename) + "\n"
end
I've got the same problem, receiving the same error. Disabling the source maps in webpack _works_, but I don't want to do that.
@dylanitorium Did you figure out a workaround? I haven't been able to get a custom BundleRenderer to work so far.
@ksweetie I did, but it wasn't the greatest and did require a custom bundle render. The problem with adding the line break between each pack, was that when the renderer went to look up the component to render, it wasn't able to find it, due to multiple contexts being present in the concatenated packs.
The solution was to extended ExecJS renderer so that it stored the compiled code for each pack in a hash, and then use the prerender options on the react_component helper to specify the pack to render the component from. And then reimplement a lot of BundleRenderer's stuff
We had a discussion in our team and decided that SSR wasn't worth it and went in a different direction, but here's what I had put together:
The renderer class:
module React
module ServerRendering
class MultiPackRenderer < ExecJSRenderer
def initialize(options = {})
@replay_console = options.fetch(:replay_console, true)
@compiled_packs = {}
filenames = options.fetch(:files, ["server_rendering.js"])
filenames.each do |filename|
js_code = BundleRenderer::CONSOLE_POLYFILL.dup
js_code << BundleRenderer::TIMEOUT_POLYFILL.dup
js_code << options.fetch(:code, "")
js_code << asset_container.find_asset(filename) + "\n"
@compiled_packs[filename] = ExecJS.compile(GLOBAL_WRAPPER + js_code)
end
end
# Prerender options are expected to be a Hash however might also be a symbol.
# pass prerender: :static to use renderToStaticMarkup
# pass prerender: true to enable default prerender
# pass prerender: {} to proxy some custom options
def render(component_name, props, prerender_options)
t_options = prepare_options(prerender_options)
t_props = prepare_props(props)
js_executed_before = before_render(component_name, t_props, t_options)
js_executed_after = after_render(component_name, t_props, t_options)
js_main_section = main_render(component_name, t_props, t_options)
js_code = compose_js(js_executed_before, js_main_section, js_executed_after)
if prerender_options.respond_to?("[]") && prerender_options[:pack_name].present?
@compiled_packs[prerender_options[:pack_name]].eval(js_code).html_safe
else
# The all inclusive context as a default
# Once everything has it's own pack, server_rendering.js can be dropped
@compiled_packs["server_rendering.js"].eval(js_code).html_safe
end
end
def before_render(_component_name, _props, _prerender_options)
@replay_console ? BundleRenderer::CONSOLE_RESET : ""
end
def after_render(_component_name, _props, _prerender_options)
@replay_console ? BundleRenderer::CONSOLE_REPLAY : ""
end
class << self
attr_accessor :asset_container_class
end
# Get an object which exposes assets by their logical path.
#
# Out of the box, it supports a Sprockets::Environment (application.assets)
# and a Sprockets::Manifest (application.assets_manifest), which covers the
# default Rails setups.
#
# You can provide a custom asset container
# with `React::ServerRendering::BundleRenderer.asset_container_class = MyAssetContainer`.
#
# @return [#find_asset(logical_path)] An object that returns asset contents by logical path
def asset_container
@asset_container ||= asset_container_class.new
end
private
def prepare_options(options)
r_func = render_function(options)
opts = case options
when Hash then options
when TrueClass then {}
else
{}
end
# This seems redundant to pass
opts.merge(render_function: r_func)
end
def render_function(opts)
if opts == :static
"renderToStaticMarkup".freeze
else
"renderToString".freeze
end
end
def prepare_props(props)
props.is_a?(String) ? props : props.to_json
end
def assets_precompiled?
!::Rails.application.config.assets.compile
end
# Detect what kind of asset system is in use and choose that container.
# Or, if the user has provided {.asset_container_class}, use that.
# @return [Class] suitable for {#asset_container}
def asset_container_class
if self.class.asset_container_class.present?
self.class.asset_container_class
elsif WebpackerManifestContainer.compatible?
WebpackerManifestContainer
elsif assets_precompiled?
if ManifestContainer.compatible?
ManifestContainer
elsif YamlManifestContainer.compatible?
YamlManifestContainer
else
# Even though they are precompiled, we can't find them :S
EnvironmentContainer
end
else
EnvironmentContainer
end
end
end
end
end
In an initialiser:
Rails.application.config.react.server_renderer = React::ServerRendering::MultiPackRenderer
In a template:
<%= react_component("MyComponent", props, prerender: { pack_name: "my_pack.js"}) %>
Hope this helps!
@dylanitorium Thank you so much for the lightning fast response! That worked for me as well. Also, agreed that server rendering has been a total PITA. =/
Here's a slimmed down version if it might help anyone else:
# config/initializers/react.rb
Rails.configuration.react.server_renderer =
Class.new(React::ServerRendering::BundleRenderer) do
def initialize(options={})
@replay_console = options.fetch(:replay_console, true)
@compiled_packs = {}
filenames = options.fetch(:files)
filenames.each do |filename|
js_code = React::ServerRendering::BundleRenderer::CONSOLE_POLYFILL.dup
js_code << React::ServerRendering::BundleRenderer::TIMEOUT_POLYFILL.dup
js_code << options.fetch(:code, "")
js_code << asset_container.find_asset(filename) + "\n"
@compiled_packs[filename] = ExecJS.compile(React::ServerRendering::ExecJSRenderer::GLOBAL_WRAPPER + js_code)
end
end
def render(component_name, props, prerender_options)
t_options = prepare_options(prerender_options)
t_props = prepare_props(props)
js_executed_before = before_render(component_name, t_props, t_options)
js_executed_after = after_render(component_name, t_props, t_options)
js_main_section = main_render(component_name, t_props, t_options)
js_code = compose_js(js_executed_before, js_main_section, js_executed_after)
@compiled_packs[prerender_options[:pack_name]].eval(js_code)
end
end
Most helpful comment
@ksweetie I did, but it wasn't the greatest and did require a custom bundle render. The problem with adding the line break between each pack, was that when the renderer went to look up the component to render, it wasn't able to find it, due to multiple contexts being present in the concatenated packs.
The solution was to extended ExecJS renderer so that it stored the compiled code for each pack in a hash, and then use the prerender options on the
react_componenthelper to specify the pack to render the component from. And then reimplement a lot of BundleRenderer's stuffWe had a discussion in our team and decided that SSR wasn't worth it and went in a different direction, but here's what I had put together:
The renderer class:
In an initialiser:
In a template:
Hope this helps!