react-rails: react_component console errors with multiple packs on a page

Steps to reproduce

Trying to use react_component where a page has 2 pack tags loaded.

example .html.erb file

<div id='inside-comp'>
  <%= javascript_pack_tag 'application' %>
  <%= javascript_pack_tag 'outside' %>
  <%= react_component("InsideComp", { greeting: "Hello" }, {prerender: false}) %>
</div>

Resulting error

1. fromRequireContextWithGlobalFallback.js:19 Error: Cannot find module './InsideComp'.
    at webpackContextResolve (.*$:14)
    at webpackContext (.*$:9)
    at fromRequireContext.js:13
    at Object.getConstructor (fromRequireContextWithGlobalFallback.js:13)
    at Object.mountComponents (index.js:85)
    at HTMLDocument.ReactRailsUJS.handleMount (index.js:132)
2. ReferenceError: InsideComp is not defined
    at eval (eval at module.exports (fromGlobal.js:13), <anonymous>:1:1)
    at module.exports (fromGlobal.js:13)
    at Object.getConstructor (fromRequireContextWithGlobalFallback.js:17)
    at Object.mountComponents (index.js:85)
    at HTMLDocument.ReactRailsUJS.handleMount (index.js:132)
3. index.js:95 Uncaught Error: Cannot find component: 'InsideComp'. Make sure your component is available to render.
    at Object.mountComponents (index.js:95)
    at HTMLDocument.ReactRailsUJS.handleMount (index.js:132)

Expected behavior

Component loads and no console errors

Actual behavior

Component loads correctly but you still get 3 console errors.

System configuration

Sprockets or Webpacker version: 3.2 React-Rails version: 2.4.2 Rect_UJS version: 2.4.2 Rails version: 5.1.2 Ruby version: 2.4.2


  1. Sample repo: https://github.com/ratneshraval/react-rails-webpacker-test
  2. Repo readme tells you 3 commands to run
  3. Visit /insidecomp and /outsidecomp
  4. Check console log.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 13
  • Comments: 19 (6 by maintainers)

Most upvoted comments

@dtom90 Thank you for looking into this.

The reason I have 2 packs and 2 contexts, is to keep individual packs size smaller and only contain relevant imports. Hence I have 2 packs

  1. component pack with app/javascript/components directory context
  2. otherComp pack with app/javascript/otherComps directory context. Now I have a page where both are needed, hence both are there, resulting in this error.

How can I achieve this? If I combine all into 1 directory context, the resulting pack will contain everything and defeat modular bundling purpose of webpack.

We managed to workaround this issue by writing our own React mount code:

app/javascript/lib/react.js

import React from 'react';
import { render, hydrate } from 'react-dom';

const CLASS_NAME_ATTR = 'data-react-class';
const PROPS_ATTR = 'data-react-props';
const RENDER_ATTR = 'data-hydrate';

export function mountComponents(context) {
  const keys = Object.keys(context);

  for (const key of keys) {
    const selector = `[${CLASS_NAME_ATTR}="${key}"]`;
    const nodes = document.querySelectorAll(selector);

    for (let i = 0; i < nodes.length; ++i) {
      const node = nodes[i];
      const component = context[key];
      const constructor = component.__esModule ? context[key].default : context[key];
      const propsJson = node.getAttribute(PROPS_ATTR);
      const props = propsJson && JSON.parse(propsJson);
      const doHydrate = node.getAttribute(RENDER_ATTR);

      if (!constructor) {
        const message = `Cannot find component in current context: '${key}'`;

        if (console && console.log) {
          console.log(`%c[react-rails] %c${message} for element,`, 'font-weight: bold', 'font-weight: normal', node);
          console.log('%c[react-rails] %cCurrent context:', 'font-weight: bold', 'font-weight: normal', context);
        }

        throw new Error(`${message}. Make sure your component is available to render.`);
      }

      const reactComponent = React.createElement(constructor, props);

      if (doHydrate && typeof hydrate === 'function') {
        hydrate(reactComponent, node);
      } else {
        render(reactComponent, node);
      }
    }
  }
}

Then in each pack, we can explicitly mount just the React components we expect to see within the page:

app/javascript/packs/application.js

import { mountComponents } from '../lib/react';

mountComponents({
  'SiteBanner': require('../components/SiteBanner'),
  'SiteSearch': require('../components/SiteSearch'),
});

app/javascript/packs/authentication.js

import { mountComponents } from '../lib/react';

mountComponents({
  'Authentication': require('../components/Authentication'),
});

This appears to work fine with the SplitChunksPlugin as well. The downside is you need to explicitly declare each component you expect to see on the page, so the dynamic discovery behavior is lost.

Basically ReactUJS will try auto-mount, you could have just wrote your own mounter JS rather than scrapped it entirely, or used standard webpack split chunks.

If someone wants to add a feature to ReactUJS to detect other instances of itself that would be helpful.

I believe this may be because you are creating two different ReactRailsUJS variables with two different directory contexts to load components:

Thus at least one of them will not be able to find the component that is being referenced.

Removing the unneeded pack tag from each view resolves the console error issue: https://github.com/dtom90/react-rails-webpacker-test/commit/192e65cddebb7a1169c7e9c2e155b66c676f508b

Of course, this may not be the solution you’re looking for, but I would say to just make sure that all the components you reference are included in the directory tree of the single context you use.

is there any solution to this? none of the above worked for me @cymen 's idea with multiple contexts sounds reasonable, can we expect that PR will be merged?