React-rails: Unable to specify multiple server rendering files when using web packer dev server

Created on 9 Oct 2019  路  4Comments  路  Source: reactjs/react-rails

Steps to reproduce

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>

Expected behavior

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.

Actual behavior

We get an error from the server:

Uncaught SyntaxError: Illegal return statement at undefined:30402:10

System configuration

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

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_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!

All 4 comments

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
Was this page helpful?
0 / 5 - 0 ratings

Related issues

adoseofjess picture adoseofjess  路  4Comments

dongtong picture dongtong  路  3Comments

scottbarrow picture scottbarrow  路  5Comments

axhamre picture axhamre  路  3Comments

prasanthrubyist picture prasanthrubyist  路  3Comments