question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Unable to specify multiple server rendering files when using web packer dev server

See original GitHub issue

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

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:2
  • Comments:5

github_iconTop GitHub Comments

2reactions
dylanitoriumcommented, Feb 18, 2020

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

1reaction
ksweetiecommented, Feb 18, 2020

@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
Read more comments on GitHub >

github_iconTop Results From Across the Web

Cannot GET / error running hello world in webpack
Clear your dev server cache ctrl + c & in task manager. My webpack.config.js file looks like this. const path = require('path'); ...
Read more >
When using react.js webpack-dev-server does not bundle
In this scenario, if you don't setup your project properly, webpack-dev-server will not automatically update your project after you change files ...
Read more >
Unable to start webpack-dev-server - Render community
It looks like the error is telling you that you don't have a dev_server: config in your webpacker.yml file. When you run webpack-dev-server...
Read more >
MiniCssExtractPlugin - webpack
This can be achieved by using the mini-css-extract-plugin , because it creates separate css files. For development mode (including webpack-dev-server ) you ...
Read more >
Server-Side Rendering - SurviveJS
Configuring webpack#. To keep things nice, we will define a separate configuration file. A lot of the work has been done already. Given...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found