reactjs / react-rails

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

Document Support for Esbuild #1149

Open IsmailM opened 2 years ago

IsmailM commented 2 years ago

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.

igortice commented 2 years ago

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

elektronaut commented 2 years ago

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";
akhilgkrishnan commented 2 years ago

Not working

multiplegeorges commented 2 years ago

Here's what I got working:

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 for `react-rails`:
```js
// 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.

cionescu commented 2 years ago

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

net1957 commented 2 years ago

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.

navidemad commented 2 years ago

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

dyeje commented 2 years ago

I made a guide based on @cionescu's repo. I will try to make a PR with updated generators and README.

net1957 commented 2 years ago

@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

justin808 commented 2 years ago

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.

louishuyng commented 2 years ago

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);
justin808 commented 2 years ago

Hi everybody, Shakapacker supports esbuild:

https://github.com/shakacode/shakapacker/blob/master/docs/using_esbuild_loader.md

christiannaths commented 2 years ago

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 "#" when prerendering Clock with {"foo":"bar"} eval (eval at ((execjs):36:8), :6:45) eval (eval at ((execjs):36:8), :18:13) (execjs):36:8 (execjs):54:14 (execjs):1:40 Object. ((execjs):1:58) Module._compile (node:internal/modules/cjs/loader:1103:14) Object.Module._extensions..js (node:internal/modules/cjs/loader:1157:10) Module.load (node:internal/modules/cjs/loader:981:32) Function.Module._load (node:internal/modules/cjs/loader:822:12) /usr/local/bundle/gems/execjs-2.8.1/lib/execjs/external_runtime.rb:39:in `exec' /usr/local/bundle/gems/execjs-2.8.1/lib/execjs/external_runtime.rb:21:in `eval' /usr/local/bundle/gems/react-rails-2.6.1/lib/react/server_rendering/exec_js_renderer.rb:39:in `render_from_parts' /usr/local/bundle/gems/react-rails-2.6.1/lib/react/server_rendering/exec_js_renderer.rb:20:in `render' /usr/local/bundle/gems/react-rails-2.6.1/lib/react/server_rendering/bundle_renderer.rb:40:in `render' /usr/local/bundle/gems/react-rails-2.6.1/lib/react/server_rendering.rb:27:in `block in render' /usr/local/bundle/gems/connection_pool-2.2.5/lib/connection_pool.rb:63:in `block (2 levels) in with' /usr/local/bundle/gems/connection_pool-2.2.5/lib/connection_pool.rb:62:in `handle_interrupt' /usr/local/bundle/gems/connection_pool-2.2.5/lib/connection_pool.rb:62:in `block in with' /usr/local/bundle/gems/connection_pool-2.2.5/lib/connection_pool.rb:59:in `handle_interrupt' /usr/local/bundle/gems/connection_pool-2.2.5/lib/connection_pool.rb:59:in `with' /usr/local/bundle/gems/react-rails-2.6.1/lib/react/server_rendering.rb:26:in `render' /usr/local/bundle/gems/react-rails-2.6.1/lib/react/rails/component_mount.rb:74:in `prerender_component' /usr/local/bundle/gems/react-rails-2.6.1/lib/react/rails/component_mount.rb:38:in `block in react_component' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/helpers/capture_helper.rb:45:in `block in capture' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/helpers/capture_helper.rb:209:in `with_output_buffer' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/helpers/capture_helper.rb:45:in `capture' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/helpers/tag_helper.rb:338:in `content_tag' /usr/local/bundle/gems/react-rails-2.6.1/lib/react/rails/component_mount.rb:57:in `react_component' /usr/local/bundle/gems/react-rails-2.6.1/lib/react/rails/view_helper.rb:21:in `react_component' /workspace/app/views/main/index.html.erb:6:in `_app_views_main_index_html_erb___4033424673020032686_24280' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/base.rb:244:in `public_send' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/base.rb:244:in `_run' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/template.rb:157:in `block in render' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/notifications.rb:208:in `instrument' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/template.rb:361:in `instrument_render_template' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/template.rb:155:in `render' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/renderer/template_renderer.rb:65:in `block (2 levels) in render_template' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/notifications.rb:206:in `block in instrument' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/notifications/instrumenter.rb:24:in `instrument' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/notifications.rb:206:in `instrument' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/renderer/template_renderer.rb:60:in `block in render_template' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/renderer/template_renderer.rb:75:in `block in render_with_layout' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/notifications.rb:206:in `block in instrument' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/notifications/instrumenter.rb:24:in `instrument' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/notifications.rb:206:in `instrument' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/renderer/template_renderer.rb:74:in `render_with_layout' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/renderer/template_renderer.rb:59:in `render_template' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/renderer/template_renderer.rb:11:in `render' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/renderer/renderer.rb:61:in `render_template_to_object' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/renderer/renderer.rb:29:in `render_to_object' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/rendering.rb:117:in `block in _render_template' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/base.rb:270:in `in_rendering_context' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/rendering.rb:116:in `_render_template' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal/streaming.rb:216:in `_render_template' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/rendering.rb:103:in `render_to_body' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal/rendering.rb:46:in `render_to_body' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal/renderers.rb:142:in `render_to_body' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/abstract_controller/rendering.rb:25:in `render' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal/rendering.rb:30:in `render' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal/instrumentation.rb:22:in `block (2 levels) in render' /usr/local/lib/ruby/3.1.0/benchmark.rb:311:in `realtime' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/core_ext/benchmark.rb:14:in `ms' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal/instrumentation.rb:22:in `block in render' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal/instrumentation.rb:91:in `cleanup_view_runtime' /usr/local/bundle/gems/activerecord-7.0.2.3/lib/active_record/railties/controller_runtime.rb:34:in `cleanup_view_runtime' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal/instrumentation.rb:21:in `render' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal/implicit_render.rb:35:in `default_render' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal/basic_implicit_render.rb:6:in `block in send_action' :90:in `tap' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal/basic_implicit_render.rb:6:in `send_action' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/abstract_controller/base.rb:214:in `process_action' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal/rendering.rb:53:in `process_action' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/abstract_controller/callbacks.rb:234:in `block in process_action' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/callbacks.rb:118:in `block in run_callbacks' /usr/local/bundle/gems/react-rails-2.6.1/lib/react/rails/controller_lifecycle.rb:31:in `use_react_component_helper' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/callbacks.rb:127:in `block in run_callbacks' /usr/local/bundle/gems/actiontext-7.0.2.3/lib/action_text/rendering.rb:20:in `with_renderer' /usr/local/bundle/gems/actiontext-7.0.2.3/lib/action_text/engine.rb:69:in `block (4 levels) in ' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/callbacks.rb:127:in `instance_exec' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/callbacks.rb:127:in `block in run_callbacks' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/callbacks.rb:138:in `run_callbacks' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/abstract_controller/callbacks.rb:233:in `process_action' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal/rescue.rb:22:in `process_action' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal/instrumentation.rb:67:in `block in process_action' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/notifications.rb:206:in `block in instrument' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/notifications/instrumenter.rb:24:in `instrument' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/notifications.rb:206:in `instrument' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal/instrumentation.rb:66:in `process_action' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal/params_wrapper.rb:259:in `process_action' /usr/local/bundle/gems/activerecord-7.0.2.3/lib/active_record/railties/controller_runtime.rb:27:in `process_action' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/abstract_controller/base.rb:151:in `process' /usr/local/bundle/gems/actionview-7.0.2.3/lib/action_view/rendering.rb:39:in `process' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal.rb:188:in `dispatch' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_controller/metal.rb:251:in `dispatch' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/routing/route_set.rb:49:in `dispatch' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/routing/route_set.rb:32:in `serve' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/journey/router.rb:50:in `block in serve' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/journey/router.rb:32:in `each' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/journey/router.rb:32:in `serve' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/routing/route_set.rb:850:in `call' /usr/local/bundle/gems/warden-1.2.9/lib/warden/manager.rb:36:in `block in call' /usr/local/bundle/gems/warden-1.2.9/lib/warden/manager.rb:34:in `catch' /usr/local/bundle/gems/warden-1.2.9/lib/warden/manager.rb:34:in `call' /usr/local/bundle/gems/rack-2.2.3/lib/rack/tempfile_reaper.rb:15:in `call' /usr/local/bundle/gems/rack-2.2.3/lib/rack/etag.rb:27:in `call' /usr/local/bundle/gems/rack-2.2.3/lib/rack/conditional_get.rb:27:in `call' /usr/local/bundle/gems/rack-2.2.3/lib/rack/head.rb:12:in `call' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/http/permissions_policy.rb:22:in `call' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/http/content_security_policy.rb:18:in `call' /usr/local/bundle/gems/rack-2.2.3/lib/rack/session/abstract/id.rb:266:in `context' /usr/local/bundle/gems/rack-2.2.3/lib/rack/session/abstract/id.rb:260:in `call' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/middleware/cookies.rb:693:in `call' /usr/local/bundle/gems/activerecord-7.0.2.3/lib/active_record/migration.rb:603:in `call' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/middleware/callbacks.rb:27:in `block in call' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/callbacks.rb:99:in `run_callbacks' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/middleware/callbacks.rb:26:in `call' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/middleware/executor.rb:14:in `call' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/middleware/actionable_exceptions.rb:17:in `call' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/middleware/debug_exceptions.rb:28:in `call' /usr/local/bundle/gems/web-console-4.2.0/lib/web_console/middleware.rb:132:in `call_app' /usr/local/bundle/gems/web-console-4.2.0/lib/web_console/middleware.rb:19:in `block in call' /usr/local/bundle/gems/web-console-4.2.0/lib/web_console/middleware.rb:17:in `catch' /usr/local/bundle/gems/web-console-4.2.0/lib/web_console/middleware.rb:17:in `call' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/middleware/show_exceptions.rb:26:in `call' /usr/local/bundle/gems/railties-7.0.2.3/lib/rails/rack/logger.rb:36:in `call_app' /usr/local/bundle/gems/railties-7.0.2.3/lib/rails/rack/logger.rb:25:in `block in call' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/tagged_logging.rb:99:in `block in tagged' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/tagged_logging.rb:37:in `tagged' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/tagged_logging.rb:99:in `tagged' /usr/local/bundle/gems/railties-7.0.2.3/lib/rails/rack/logger.rb:25:in `call' /usr/local/bundle/gems/sprockets-rails-3.4.2/lib/sprockets/rails/quiet_assets.rb:13:in `call' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/middleware/remote_ip.rb:93:in `call' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/middleware/request_id.rb:26:in `call' /usr/local/bundle/gems/rack-2.2.3/lib/rack/method_override.rb:24:in `call' /usr/local/bundle/gems/rack-2.2.3/lib/rack/runtime.rb:22:in `call' /usr/local/bundle/gems/activesupport-7.0.2.3/lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/middleware/executor.rb:14:in `call' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/middleware/static.rb:23:in `call' /usr/local/bundle/gems/rack-2.2.3/lib/rack/sendfile.rb:110:in `call' /usr/local/bundle/gems/actionpack-7.0.2.3/lib/action_dispatch/middleware/host_authorization.rb:137:in `call' /usr/local/bundle/gems/railties-7.0.2.3/lib/rails/engine.rb:530:in `call' /usr/local/bundle/gems/puma-5.6.2/lib/puma/configuration.rb:252:in `call' /usr/local/bundle/gems/puma-5.6.2/lib/puma/request.rb:77:in `block in handle_request' /usr/local/bundle/gems/puma-5.6.2/lib/puma/thread_pool.rb:340:in `with_force_shutdown' /usr/local/bundle/gems/puma-5.6.2/lib/puma/request.rb:76:in `handle_request' /usr/local/bundle/gems/puma-5.6.2/lib/puma/server.rb:441:in `process_client' /usr/local/bundle/gems/puma-5.6.2/lib/puma/thread_pool.rb:147:in `block in spawn_thread' ```

I 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?

Petercopter commented 2 years ago

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.

guyas commented 2 years ago

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?

dyeje commented 2 years ago

@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));
guyas commented 2 years ago

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>

mrpineapples commented 1 year ago

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

justin808 commented 1 year ago

@mrpineapples Thanks!

@ahangarha we need this in the docs.

justin808 commented 1 year ago

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?

mrpineapples commented 1 year ago

@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;
satoko2pac commented 1 year ago

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]
}
imjared commented 1 year ago

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.

SeanRoberts commented 11 months ago

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

imjared commented 11 months ago

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

SeanRoberts commented 11 months ago

@imjared What did you end up choosing to do? Just deliver a large bundle?

imjared commented 11 months ago

@SeanRoberts for now, yeah. we're considering looking into shakapacker but time is a limited resource.

ahangarha commented 11 months ago

Migration to Shakapacker shouldn't be challenging. But you may also consider outsourcing it to our developers in Shakacode.

MrNagoo commented 10 months ago

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.

justin808 commented 10 months ago

Any chance that somebody could summarize for the project docs?

adifsgaid commented 2 months ago

Ping @justin808

Issue with Dynamic Importing of Components in Rails 7 with ESBuild

Context

Rails 7 has been gradually moving away from Webpack, offering various JavaScript bundling options, including ESBuild. This shift prompted an exploration of non-Webpack solutions for integrating React with Rails using the react-rails gem.

Problem

The typical method for loading components dynamically (require.context()) is specific to Webpack, and adapting this to ESBuild required a different approach.

Solution

I followed a similar workaround to what @multiplegeorges suggested, but adjusted for my case. Here's the effective setup:

Component Registration and Importing

Instead of relying on require.context(), I used ESBuild's import globbing to dynamically import and register React components.

// app/javascript/application.js
import * as Components from "./components/**/*.{js,ts,tsx,jsx}";
const componentsContext = {};

Components.default.forEach((component) => {
  let cleanName = component.filename
    .replace("./components/", "")
    .replace(/\.\w+$/, ""); // Strips the file extension
  componentsContext[cleanName] = component.module.default;
});

const ReactRailsUJS = require("react_ujs");
ReactRailsUJS.getConstructor = (name) => componentsContext[name];

Configuration for ESBuild Here's an example of how I configured ESBuild to bundle the JavaScript, including the dynamic imports:

// esbuild.config.mjs
import * as esbuild from "esbuild";
import path from "path";
import rails from "esbuild-rails";
import chokidar from "chokidar";
import http from "http";
import { setTimeout } from "timers/promises";
import ImportGlobPlugin from "esbuild-plugin-import-glob";

esbuild.build({
  entryPoints: ["application.js"],
  bundle: true,
  outdir: "app/assets/builds",
  absWorkingDir: path.join(process.cwd(), "app/javascript"),
  plugins: [ImportGlobPlugin()],
  format: "esm",
  minify: process.env.RAILS_ENV === "production",
  sourcemap: process.env.RAILS_ENV !== "production",
  splitting: true,
}).catch(() => process.exit(1));

And my components:

// javascript/components/Post.jsx
import React from 'react';

export default function Post({ title }) {
  return (
    <h1 className="text-2xl font-bold">Hey, <span className="underline">{title}</span></h1>
  );
}

I hope it helps 😊