react-rails
react-rails copied to clipboard
Is ReactRailsUJS.serverRender intended to be overridable?
Steps to reproduce
N/A
Expected behavior
N/A
Actual behavior
N/A
System configuration
Sprockets or Webpacker version: N/A React-Rails version: 2.4.3 Rect_UJS version: 2.4.3 Rails version: 4.2.8 Ruby version: 2.4.3
I'm trying to set up server rendering on a project that uses react-rails
along with styled-components
. styled-components
needs to be able to add a <style>
tag to the HTML, and it has an API for use with server-side rendering which can be used to generate an appropriate <style>
tag as a string.
My goal is to be able to render the <style>
tag along with the component HTML, so that the prerendered HTML has styling before the JavaScript loads on the client. Unfortunately, it's not easy to do this with the current react-rails
API, because the react_component
helper always returns only the one rendered component without any additional HTML. I was able to get it to work by monkeypatching ReactRailsUJS.serverRender
:
import { ReactDOMServer } from 'react-dom/server';
import { ServerStyleSheet } from 'styled-components';
ReactRailsUJS.serverRender = function(renderFunction, componentName, props) {
const ComponentConstructor = this.getConstructor(componentName);
const stylesheet = new ServerStyleSheet();
const wrappedElement = stylesheet.collectStyles(<ComponentConstructor {...props} />);
const text = ReactDOMServer[renderFunction](wrappedElement);
// prepend the style tags to the component HTML
return stylesheet.getStyleTags() + text;
};
I'd like to avoid monkeypatching the ReactRailsUJS
object in production, since I'm relying on undocumented behavior that could change in a future release. However, it doesn't seem like there is another way to do this while still using react-rails
to prerender components. I noticed that ReactRailsUJS.getConstructor
is documented to be patchable -- would it be possible to explicitly make serverRender
patchable as well? Alternatively, is there another API I should use to prerender style tags?
@not-an-aardvark Thanks for reaching out on this one, styled-components seem interesting and I hadn't seen them before.
I will probably mark the serverRender
method as OK to be overwritten, It's not changed since it was written and is likely to only change if React updating forces us to. Additionally I'd accept a PR that added before/after serverRender hooks that would allow the above method as I can imagine others that may want to modify the text that gets rendered out.
@not-an-aardvark Would you mind if I modified your example to put in the wiki? It looks like a solution like this may help #860
Sure, feel free.
@not-an-aardvark did you do this inside server_rendering.js?
I'm curious exactly where you monkey patched this, especially with your usage of JSX.
We're trying to do this inside server_rendering.js, and it's not working. We're actually reassigning the default value there as well, and this also isn't working:
ReactRailsUJS.serverRender = function(renderFunction, componentName, props) {
const componentClass = this.getConstructor(componentName)
const element = React.createElement(componentClass, props)
return ReactDOMServer[renderFunction](element)
}
also @not-an-aardvark and @BookOfGreg one thing about this implementation, is the style tags go inside the component, so on client hydration they are removed.
@reywright I tried this monkey patch in my app, and succeeded SSR of styles. In my code, changed import like below.
- import { ReactDOMServer } from "react-dom/server";
+ import ReactDOMServer from "react-dom/server";
And full of my server_rendering.js
is
// By default, this pack is loaded for server-side rendering.
// It must expose react_ujs as `ReactRailsUJS` and prepare a require context.
import React from "react";
import ReactDOMServer from "react-dom/server";
import { ServerStyleSheet } from "styled-components";
const componentRequireContext = require.context("components", true);
const ReactRailsUJS = require("react_ujs");
ReactRailsUJS.useContext(componentRequireContext);
ReactRailsUJS.serverRender = function(renderFunction, componentName, props) {
const ComponentConstructor = this.getConstructor(componentName);
const stylesheet = new ServerStyleSheet();
const wrappedElement = stylesheet.collectStyles(
<ComponentConstructor {...props} />
);
const text = ReactDOMServer[renderFunction](wrappedElement);
// prepend the style tags to the component HTML
return stylesheet.getStyleTags() + text;
};
Same result: lib/appstate_renderer.rb
module React
module ServerRendering
class AppstateRenderer < BundleRenderer
def render(component_name, props, prerender_options)
html = super(component_name, props, prerender_options)
script_html = ActiveSupport::SafeBuffer.new "<style>.block-color {color: black;}</style>"
html = script_html + html
end
end
end
end
config/initializers/react.rb
require "#{Rails.root}/lib/appstate_renderer"
Rails.application.config.react.server_renderer = React::ServerRendering::AppstateRenderer
@bakunyo Using your technique, a style tag is added above the component, but when the component is hydrated, React strips the style tag with the warning:
Warning: Did not expect server HTML to contain a <style> in <div>.
Did you manage to work around this?
@Undistraction Yes, that's right. So my app use forceful technique in rails view.
Extracting style
tag from div
after react_component
, render each tags where I want.
Below is a sample code.
app/views/ssr/index.html.erb
<% body = react_component("App", @props, { prerender: true, tag: 'div' }) %>
<% style = body.slice!(/<style.+style>/m) %>
<%= body.html_safe %>
<% content_for :style do %>
<% style.html_safe %>
<% end %>
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
# some head tags ...
<%= yield :style %>
</head>
<body>
<%= yield %>
</body>
</html>
@bakunyo Thanks for taking the time to document your approach. I feared something like this would be the solution. I think I'm going to open up a feature request for an API allowing a more sensible approach.
Closing the issue. We can find the solution in #922.