Closed ratneshraval closed 2 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.
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.
@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
app/javascript/components
directory context 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.
@ratneshraval did you ever solve this?
@noisyscanner I was not able to solve so I stopped using this.
That sucks :/ I think we are going to have to do the same - just use ReactDOM.render
unless we need SSR
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.
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.
@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)
@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.
@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!
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.
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.
@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
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.
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
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?
Steps to reproduce
Trying to use
react_component
where a page has 2 pack tags loaded.example .html.erb file
Resulting error
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