reactjs / react-rails

Integrate React.js with Rails views and controllers, the asset pipeline, or webpacker.
Apache License 2.0
6.75k stars 759 forks source link

react_component console errors with multiple packs on a page #885

Closed ratneshraval closed 2 years ago

ratneshraval commented 6 years ago

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.
ttanimichi commented 6 years ago

👀

dtom90 commented 6 years ago

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.

BookOfGreg commented 6 years ago

Nice work and thanks @dtom90 👍

Wonder if in an application with multiple packs, the ReactRailsUJS should be code-split out into a global pack, and then component packs could be loaded independently on each page. That should stop any double ReactRailsUJS loads and allow component blobs at will.

If so then there's an easy PR for someone to add to the Readme, or to add to the Wiki.

ratneshraval commented 5 years ago

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

noisyscanner commented 5 years ago

@ratneshraval did you ever solve this?

ratneshraval commented 5 years ago

@noisyscanner I was not able to solve so I stopped using this.

noisyscanner commented 5 years ago

That sucks :/ I think we are going to have to do the same - just use ReactDOM.render unless we need SSR

ratneshraval commented 5 years ago

I ended converting all pages to CSR (webpacker + react router and componentMount to fetch props) This also made on-boarding easier for devs who came from other React (non-rails) background.

BookOfGreg commented 5 years ago

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.

tim-millar commented 5 years ago

@ratneshraval can you give an example of your approach? we are trying to split our JS bundle and are running into the same problem (rendering client-side)

ratneshraval commented 5 years ago

@tim-millar I don't have a simple example repository to share right now. If you have a single page app then you can use split chunks like @BookOfGreg suggested. I used rails routes with react-router to create multiple-SPA architecture where each miniSPA will load the relevant pack. Hope this makes sense, if not then I'll try to create some sample repo.

tim-millar commented 5 years ago

@ratneshraval We have more of a multi-SPA using react-router approach. What we'd like to do is have some components in a pack which is loaded in the application.html (i.e., on every page, like the navigation), and other components which will be loaded on a case-by-case basis in the view (e.g., a user's profile). This results in sometimes having two (or more) javascript_pack_tag calls on the same page, with the errors described in the OP. If there's a way round this which would allow us to split code using webpacker and continue to use the react_component helper, I'd love to know what it is!

tomkr4l commented 5 years ago

I don't know if It's correct approach, however as a workaround we've written helper, to load js files based on controller. So we have something like this:

# application_helper.rb
def javascript_packs_selector(controller_name)
  packs = ['application']
  if controller_name == 'hotels'
    packs << 'show_tour'
  elsif controller_name == 'news'
    packs << 'newsletter'
  end
  packs.uniq
end
# application.html.haml
= javascript_pack_tag *javascript_packs_selector(controller_name)

This way we can controll which js files will be included on different pages and avoid multiple javascript_pack_tag calls.

ratneshraval commented 5 years ago

While the original issue still exists, I have been thinking about other ways to reduce size, and thought of the react suspense. Here is the official guide link. https://reactjs.org/docs/code-splitting.html#reactlazy It also mentions server side rendering and recommends looking at loadable-components

I have not used either in my projects yet but for client side, lazy loading and suspense seems worth trying.

sharvy commented 4 years ago

@ratneshraval This is my workaround: _packs/shared_andinternal.js

var ReactRailsUJS = require("react_ujs");

var sharedComponentsRequireContext = require.context("shared_components", true);
var externalComponentsRequireContext = require.context("internal_components", true);

ReactRailsUJS.useContext(sharedComponentsRequireContext);
ReactRailsUJS.useContext(externalComponentsRequireContext);

Then in the layout: <%= javascript_pack_tag 'shared_and_internal' %>

But the downside is, I won't be able to use the power of Webpack 4 SplitChunksPlugin

scraton commented 4 years ago

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.

cymen commented 3 years ago

I tried the approach @sharvy outlined above but couldn't get it working properly -- only modules the last useContext loaded were resolvable. I debated how to fix this and decided to add support for useContexts which takes an array of require.context and attempts to resolve the required module in each context before going to the fallbacks.

I went to go make a PR but unfortunately, it looks like the node modules within the test rails app are so outdated I can't install them with Node 14.x. I'll try upgrading them first so I can write some tests in order to get a PR in.

In the meantime, I published a fork of the npm module as react_ujs_multicontext and the source for that is here: https://github.com/cymen/react-rails/tree/master/react_ujs

The addition of useContexts is in this file: https://github.com/cymen/react-rails/blob/master/react_ujs/src/getConstructor/fromRequireContextsWithGlobalFallback.js

I'll get a PR in if I can get the test suite running.

Update: added PR here https://github.com/reactjs/react-rails/pull/1144

Teckden commented 2 years ago

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?

alkesh26 commented 2 years ago

Are we still facing this issue? Let's close it for now. Based on the above conversation we have a solution here.