You can render React components inside your Rails server with prerender: true
:
<%= react_component('HelloMessage', {name: 'John'}, {prerender: true}) %>
<!-- becomes: -->
<div data-react-class="HelloMessage" data-react-props="{"name":"John"}">
<h1>Hello, John!</h1>
</div>
(It will also be mounted by the UJS on page load.)
Server rendering is powered by ExecJS
and subject to some requirements:
react-rails
must load your code. By convention, it usesserver_rendering.js
, which was created by the install task. This file must include your components and their dependencies (eg, Underscore.js).- Requires separate compilations for server & client bundles (see Webpack config)
- Your code can't reference
document
orwindow
. Prerender processes don't have access todocument
orwindow
, so jQuery and some other libs won't work in this environment :(
ExecJS
supports many backends. CRuby users will get the best performance from mini_racer
.
Server renderers are stored in a pool and reused between requests. Threaded Rubies (eg jRuby) may see a benefit to increasing the pool size beyond the default 0
.
These are the default configurations:
# config/application.rb
# These are the defaults if you don't specify any yourself
module MyApp
class Application < Rails::Application
# Settings for the pool of renderers:
config.react.server_renderer_pool_size ||= 1 # ExecJS doesn't allow more than one on MRI
config.react.server_renderer_timeout ||= 20 # seconds
config.react.server_renderer = React::ServerRendering::BundleRenderer
config.react.server_renderer_options = {
files: ["server_rendering.js"], # files to load for prerendering
replay_console: true, # if true, console.* will be replayed client-side
}
# Changing files matching these dirs/exts will cause the server renderer to reload:
config.react.server_renderer_extensions = ["jsx", "js"]
config.react.server_renderer_directories = ["/app/assets/javascripts", "/app/javascript/"]
end
end
Some of ExecJS's backends are stateful (eg, mini_racer, therubyracer). This means that any side-effects of a prerender will affect later renders with that renderer.
To manage state, you have a couple options:
- Make a custom renderer with
#before_render
/#after_render
hooks as described below - Use
per_request_react_rails_prerenderer
to manage state for a whole controller action.
To check out a renderer for the duration of a controller action, call the per_request_react_rails_prerenderer
helper in the controller class:
class PagesController < ApplicationController
# Use the same React server renderer for the entire request:
per_request_react_rails_prerenderer
end
Then, you can access the ExecJS context directly with react_rails_prerenderer.context
:
def show
react_rails_prerenderer # => #<React::ServerRendering::BundleRenderer>
react_rails_prerenderer.context # => #<ExecJS::Context>
# Execute arbitrary JavaScript code
# `self` is the global context
react_rails_prerenderer.context.exec("self.Store.setup()")
render :show
react_rails_prerenderer.context.exec("self.Store.teardown()")
end
react_rails_prerenderer
may also be accessed in before- or after-actions.
react-rails
depends on a renderer class for rendering components on the server. You can provide a custom renderer class to config.react.server_renderer
. The class must implement:
#initialize(options={})
, which accepts the hash fromconfig.react.server_renderer_options
#render(component_name, props, prerender_options)
to return a string of HTML
react-rails
provides two renderer classes: React::ServerRendering::ExecJSRenderer
and React::ServerRendering::BundleRenderer
.
ExecJSRenderer
offers two other points for extension:
#before_render(component_name, props, prerender_options)
to return a string of JavaScript to execute before callingReact.render
#after_render(component_name, props, prerender_options)
to return a string of JavaScript to execute after callingReact.render
Any subclass of ExecJSRenderer
may use those hooks (for example, BundleRenderer
uses them to handle console.*
on the server).