react-rails icon indicating copy to clipboard operation
react-rails copied to clipboard

Is ReactRailsUJS.serverRender intended to be overridable?

Open not-an-aardvark opened this issue 6 years ago • 9 comments

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 avatar Jan 25 '18 21:01 not-an-aardvark

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

BookOfGreg avatar Jan 29 '18 08:01 BookOfGreg

@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

BookOfGreg avatar Jan 29 '18 08:01 BookOfGreg

Sure, feel free.

not-an-aardvark avatar Jan 29 '18 15:01 not-an-aardvark

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

silouanwright avatar Feb 26 '18 19:02 silouanwright

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

bakunyo avatar Jun 05 '18 15:06 bakunyo

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

programrails avatar Aug 09 '18 15:08 programrails

@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 avatar Aug 13 '18 13:08 Undistraction

@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 avatar Aug 13 '18 15:08 bakunyo

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

Undistraction avatar Aug 13 '18 15:08 Undistraction

Closing the issue. We can find the solution in #922.

alkesh26 avatar Nov 02 '22 14:11 alkesh26