react-rails
react-rails copied to clipboard
Document Support for Esbuild
Rails 7 seems to be moving away from webpack(er), with multiple options to bundle JS with the new js-bundling
gem (e.g. with ESbuild etc.).
As such, in order to future proof the project, it might make sense to add support/documentation for JS bundling solutions other than webpack.
Notably, require.context()
is webpack specific. I have managed to work around this by providing a mapping manually with ESbuild:
var ReactRailsUJS = require('react_ujs');
ReactRailsUJS.useContext({
Table: require('./components/Table'),
});
As such, simply removing require.context()
meant that I was able to successfully use react-rails with ESbuild.
This took quite some time to work out - as such, it would be useful to document this (or a better solution) somewhere.
Rails 7 seems to be moving away from webpack(er), with multiple options to bundle JS with the new
js-bundling
gem (e.g. with ESbuild etc.).As such, in order to future proof the project, it might make sense to add support/documentation for JS bundling solutions other than webpack.
Notably,
require.context()
is webpack specific. I have managed to work around this by providing a mapping manually with ESbuild:var ReactRailsUJS = require('react_ujs'); ReactRailsUJS.useContext({ Table: require('./components/Table'), });
As such, simply removing
require.context()
meant that I was able to successfully use react-rails with ESbuild.This took quite some time to work out - as such, it would be useful to document this (or a better solution) somewhere.
for me not work
Going by the README, it seems like providing your own getConstructor
function is the cleanest solution. Here's how I solved it:
const ReactRailsUJS = require("react_ujs");
import * as Components from "./components";
ReactRailsUJS.getConstructor = (className) => Components[className];
With ./components.js
being a manifest of all the components that can be mounted:
export { default as Component1 } from "./components/Component1";
export { default as Component2 } from "./components/Component2";
export { default as Component3 } from "./components/Component3";
Not working
Here's what I got working:
- Install the esbuild-plugin-import-glob plugin
- Create a
esbuild.config.js
file in the root of your project. - Put the following in the your
esbuild.config.js
:
const path = require('path')
const ImportGlobPlugin = require('esbuild-plugin-import-glob').default;
const esbuild = require("esbuild")
esbuild.build({
entryPoints: ["application.js"],
bundle: true,
outdir: path.join(process.cwd(), "app/assets/builds"),
absWorkingDir: path.join(process.cwd(), "app/javascript"),
watch: true,
minify: false,
plugins: [
ImportGlobPlugin()
],
}).catch(() => process.exit(1))
- In
package.json
change your build command to:"build": "node esbuild.config.js"
. - Now in your
app/javascripts/application.js
add this config forreact-rails
:
// NOTE: I am using Typescript and tsx files here. Change for your setup.
import * as Components from "./components/**/*.tsx"
let componentsContext = {}
Components.filenames.forEach((fileName, i) => {
let cleanName = fileName.replace("./components/", "").replace(".tsx", "")
componentsContext[cleanName] = Components.default[i].default
})
const ReactRailsUJS = require("react_ujs")
console.log(ReactRailsUJS)
ReactRailsUJS.getConstructor = (name) => {
return componentsContext[name]
}
ReactRailsUJS.handleEvent('turbo:load', ReactRailsUJS.handleMount, false);
ReactRailsUJS.handleEvent('turbo:frame-load', ReactRailsUJS.handleMount, false);
ReactRailsUJS.handleEvent('turbo:before-render', ReactRailsUJS.handleUnmount, false);
The key is globbing your files through the plugin, building up your own context object, and then providing your own custom getConstructor
function so that ReacRailsUJS can find the correct component when the name is provided.
I bet there are nicer approaches, but one that worked for me (heavily inspired by @multiplegeorges) with a fresh rails7 & esbuild app is:
app/javascript/components/index.js
import components from "./**/*.js"
let componentsContext = {}
components.forEach((component) => {
componentsContext[component.name.replace(".js", "")] = component.module.default
})
const ReactRailsUJS = require("react_ujs")
ReactRailsUJS.getConstructor = (name) => {
return componentsContext[name]
}
ReactRailsUJS.handleEvent('turbo:load', ReactRailsUJS.handleMount, false);
ReactRailsUJS.handleEvent('turbo:frame-load', ReactRailsUJS.handleMount, false);
ReactRailsUJS.handleEvent('turbo:before-render', ReactRailsUJS.handleUnmount, false);
@multiplegeorges's approach was throwing a warning for me: Import "filenames" will always be undefined because the file "components/index.js" has no exports
.
working example: https://github.com/cionescu/rails-7-new-esbuild/blob/e50cdf3bd790ba26ffcf5bff7f0596aac4d1173c/app/javascript/components/index.js
I'm using the related project webpacker-react and tried it on a test project on rails 7 with esbuild (jsbundling-rails).
apart adding lodash in package.json it work out the box.
see https://github.com/renchap/webpacker-react
Perhaps it could help to modify this project.
@net1957 So you are running webpacker and esbuild at the same time ? It will be awesome if someone can edit the README.md with how to support esbuild 🥇
I made a guide based on @cionescu's repo. I will try to make a PR with updated generators and README.
@navidemad No, I dropped webpacker in favor of esbuild, but webpacker-react don't depend on webpacker. The name is a little misleading.
this gem and webpacker-react resolve the same problem in Rails with the same interface in controllers and views
I'm maintaining shakapacker
, the successor to rails/webpacker, which includes everything that was going into webpacker v6.
What's the advantage of moving away from webpacker? I just updated the comparison: https://github.com/rails/jsbundling-rails/pull/79.
this one working fine with me
import components from './react/**/**.tsx';
let componentsContext = {};
components.forEach(component => {
const name = Object.keys(component)[0];
componentsContext[name] = component[name];
});
ReactRailsUJS.getConstructor = name => {
return componentsContext[name];
};
ReactRailsUJS.handleEvent('turbo:load', ReactRailsUJS.handleMount, false);
ReactRailsUJS.handleEvent('turbo:frame-load', ReactRailsUJS.handleMount, false);
ReactRailsUJS.handleEvent('turbo:before-render', ReactRailsUJS.handleUnmount, false);
Hi everybody, Shakapacker supports esbuild:
https://github.com/shakacode/shakapacker/blob/master/docs/using_esbuild_loader.md
Big thanks to @multiplegeorges and @cionescu for the workaround, I managed to get components loaded and working after cloning @cionescu's example, however it seems to choke on prerender: <%= react_component 'Clock', {foo: 'bar'}, {prerender: true} %>
Encountered error "#<ExecJS::ProgramError: TypeError: Cannot read properties of undefined (reading 'serverRender')>" when prerendering Clock with {"foo":"bar"}
eval (eval at <anonymous> ((execjs):36:8), <anonymous>:6:45)
Click to expand and see the full backtrace
``` Encountered error "#<:programerror: typeerror: cannot read properties of undefined>" when prerendering Clock with {"foo":"bar"} eval (eval atI did notice one or two other issues here suggesting that the react-server
js file from the gem needs to be imported before the react_ujs
file, which does solve a similar issue when using sprockets, but not with esbuild 😕
Anyone have any ideas?
I struggled to get the import globbing to work with https://github.com/thomaschaaf/esbuild-plugin-import-glob
I ended up installing https://github.com/excid3/esbuild-rails
Then I did this in my esbuild.config.js
:
const path = require('path')
const rails = require('esbuild-rails')
require('esbuild')
.build({
absWorkingDir: path.join(process.cwd(), 'app/javascript'),
bundle: true,
entryPoints: ['application.js'],
minify: true,
outdir: path.join(process.cwd(), 'app/assets/builds'),
plugins: [rails()],
watch: process.argv.includes('--watch')
})
.catch(() => process.exit(1))
And that seems to work so far.
I'm trying to follow @dyeje 's instructions, but I can't find any file named esbuild.config.js
.
ran the following commands with node v16.15.1:
rails new esb_app -j esbuild //this is a rails v7.0.3 app
cd esb_app
//(added 'react-rails' to the Gemfile)
bundle install
npm i esbuild-plugin-import-glob@^0.1.1
npm i react@^17.0.2
npm i react-dom@^17.0.2
npm i react_ujs@^2.6.1
Where is this file supposed to be?
@guyas make it in the root of your project. Here's an example file from one of my projects:
const path = require("path");
const rails = require("esbuild-rails");
const ImportGlobPlugin = require("esbuild-plugin-import-glob").default;
require("esbuild")
.build({
entryPoints: ["application.js"],
bundle: true,
outdir: path.join(process.cwd(), "app/assets/builds"),
absWorkingDir: path.join(process.cwd(), "app/javascript"),
watch: process.argv.includes("--watch"),
plugins: [rails(), ImportGlobPlugin()],
loader: { ".js": "jsx" },
})
.catch(() => process.exit(1));
I've created such file and went on with your guide, yet the component is not rendered as intended. I still get only
<div data-react-class="HelloWorld" data-react-props="{"html_options":{"prerender":true}}" data-react-cache-id="HelloWorld-0"></div>
@guyas Idk if you were running into the same issue as I was but I was using named exports for my react components, after I switched to default exports my components were able to render fine!
@mrpineapples Thanks!
@ahangarha we need this in the docs.
Note, given that https://github.com/shakacode/shakapacker#esbuild-loader-configuration supports ESBuild, is there any reason to add complexity to also support jsbundling-rails?
@justin808 Just for posterity it doesn't actually need to be a default export, the code I used which was in this example https://github.com/reactjs/react-rails/issues/1149#issuecomment-1003318505 uses
componentsContext[component.name.replace(".js", "")] = component.module.default
which can be modified to support named and/or default exports by doing something like this
const componentName = component.name.replace(".jsx", "");
// We prefer named exports but fall back to default
componentsContext[componentName] = component.module[componentName] || component.module.default;
In build options I set minify: true
in production build, then I also needed to set keepNames: true
Doc
Otherwise minification renames component's name and cannot find component
error occurred!
require('esbuild').build({
...
bundle: true,
outdir: 'app/assets/builds',
publicPath: 'assets',
minify: true,
// add this
keepNames: true
...
}
import reactComponents from "./react/**/**.tsx"
let componentsContext = {}
reactComponents.forEach((component) => {
// maybe component.default.name is renamed without keepNames: true
componentsContext[component.default.name] = component.default
})
const ReactRailsUJS = require("react_ujs")
ReactRailsUJS.getConstructor = (name) => {
return componentsContext[name]
}
Working on a bit of legacy code and unfortunately with the solutions here, we've found ourselves with a mega-bundle of all our components in application.js
that clocks in at nearly 5mb. Has anyone found a way to combo esbuild + react-rails and get reliable code-splitting? Even ideas for an approach would be appreciated.
Working on a bit of legacy code and unfortunately with the solutions here, we've found ourselves with a mega-bundle of all our components in
application.js
that clocks in at nearly 5mb. Has anyone found a way to combo esbuild + react-rails and get reliable code-splitting? Even ideas for an approach would be appreciated.
@imjared Did you get anywhere with this? One naive thought would be to manually manage different bundles for the different pages that you need but that sounds like a pretty big pain
@SeanRoberts - unfortunately not. i did briefly consider the different bundle/different page approach but with all the prop passing and whatnot that we got out of the box in react-rails, i wasn't too sure how it'd work out.
@imjared What did you end up choosing to do? Just deliver a large bundle?
@SeanRoberts for now, yeah. we're considering looking into shakapacker but time is a limited resource.
Migration to Shakapacker shouldn't be challenging. But you may also consider outsourcing it to our developers in Shakacode.
Moved to jsbundling-rails with esbuild. removed scss from webpacker and let dartsass-rails handle it. then removed webpacker. React-rails was the last to get working and the first comment was the ticket! I didn't even create an esbuild.config... although I could. here's my script
"build": "esbuild app/javascript/*.* --bundle --sourcemap --loader:.png=file --loader:.svg=file --loader:.js=jsx --outdir=app/assets/builds --public-path=assets --define:global=window --define:process='{}' --define:module='{}' --define:process.env.NODE_ENV='\"production\"' --platform=browser"
Things that didn't work...
jsbundling-rails && cssbundling-rails (without unbundling sass) vite-rails shakapacker
These solutions would probably work with a simpler app (or more time configuring them) but I'm using an old app with millions of users and almost a decade of all kinds of developers
now i'm set up to switch to cssbundling-rails and propshaft using tailwind. Sky's the limit.
Any chance that somebody could summarize for the project docs?