React-rails: Is ReactRailsUJS.serverRender intended to be overridable?

Created on 25 Jan 2018  路  9Comments  路  Source: reactjs/react-rails

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?

enhancement good first issue

Most helpful comment

@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;
};

All 9 comments

@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?

code

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

davidlormor picture davidlormor  路  3Comments

rstudner picture rstudner  路  5Comments

FreeApophis picture FreeApophis  路  4Comments

tbrd picture tbrd  路  5Comments

wenwei63029869 picture wenwei63029869  路  3Comments