reactjs / react-rails

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

Support for Vite #1134

Open ksweetie opened 3 years ago

ksweetie commented 3 years ago

This is a feature suggestion, and I imagine it would be a ton of work, so I mainly just wanted to start a thread for discussion.

React-Rails already supports both Sprockets and Webpack. Adding support for Vite would be great. There's a relatively new vite_ruby gem, and the author there commented on the feasibility of using it with react-rails. In my case, the app I wanted to try it with uses SSR, so I didn't get very far in testing it out.

Any thoughts?

BookOfGreg commented 3 years ago

I'd be interested in seeing the popularity of vite or other packaging tools as I've not used it before. I'd also be interested in making sure one maintainer uses vite primarily so that the points brought up by @ElMassimo are kept in mind when working on it. The point about using glob for context is something that I'm personally unaware of purely due to unfamiliarity for instance.

So this is a +1 if it's popular or upcoming and we have someone who is qualified to maintain this gem that would use vite regularly.

pustomytnyk commented 3 years ago

In development mode Vite seem to use esbuild". It is possible to import components for esbuild using plugin similar to https://github.com/rails/jsbundling-rails/compare/main...xiaohui-zhangxh:main (note how Stimulus controllers are imported by @xiaohui-zhangxh)

I tried this plugin (/components path and .jsx hardcoded)

const componentPath = (module) => module.replace('./components/', '').replace('.jsx', '')
const glob  = require('glob').sync
// thanks: https://github.com/thomaschaaf/esbuild-plugin-import-glob
// thanks: https://github.com/rails/jsbundling-rails/compare/main...xiaohui-zhangxh:main
const ImportGlobPlugin = () => ({
  name: 'require-context',
  setup: (build) => {
    build.onResolve({ filter: /\*/ }, async (args) => {
      if (args.resolveDir === '') {
        return; // Ignore unresolvable paths
      }
      return {
        path: args.path,
        namespace: 'import-glob',
        pluginData: {
          resolveDir: args.resolveDir,
        },
      };
    });

    build.onLoad({ filter: /.*/, namespace: 'import-glob' }, async (args) => {
      const files = (
        glob(args.path, {
          cwd: args.pluginData.resolveDir,
        })
      ).sort();

      let importerCode = `
        ${files
        .map((module, index) => {
          return `import * as module${index} from '${module}'`})
        .join(';')}
        export default [${files
        .map((module, index) => `module${index}.default`)
        .join(',')}];
        export const context = {
          ${files.map((module, index) => `'${componentPath(module)}': module${index}.default`).join(',')}
        }
      `;

      return { contents: importerCode, resolveDir: args.pluginData.resolveDir };
    });
  },
});

Then in entrypoint this makes my components globally visible to react-rails

import { context } from './components/**/*.jsx';
Object.keys(context).forEach((key) => {
  window[key] = context[key]
})
pacMakaveli commented 2 years ago

Couldn't fix this using @pustomytnyk solution as it errors out because require is not defined. This seems to work for me.

  var context = import.meta.globEager('../components/*.{js,jsx}');

  Object.keys(context).forEach((path) => {
    let component = context[path].default;

    `import * as ${ component.name } from '${ path }'`;  

    window[component.name] = component;
  });
Alxzu commented 2 years ago

@pacMakaveli where that should be located? entrypoint? Thanks

pacMakaveli commented 2 years ago

@pacMakaveli where that should be located? entrypoint? Thanks

I've put it in my main application.js. app/packs/entrypoints/application.js

Alxzu commented 2 years ago

Did anyone manage to implement vite + SSR successfully?

ElMassimo commented 2 years ago

@Alxzu Yes, although not in the context of react-rails. See this example with Inertia.js and React.

Amit89480 commented 1 year ago

Can i help in fixing this issue?

memoxmrdl commented 1 year ago

Hi!

I found a possible solution (for Vite Ruby) on this line number https://github.com/reactjs/react-rails/blob/master/react_ujs/index.js#L79 I saw that and understood how it was made the constructor so, I take the code and it change a little bit and I do the following approach:

// app/javascript/helpers/viteConstructorRequireContext.js

export const viteConstructorRequireContext = function(reqCtx) {
  const fromRequireContext = function(reqCtx) {
    return function(className) {
      var parts = className.split(".");
      var filename = parts.shift();
      var keys = parts;
      // Load the module:
      var componentPath = Object.keys(reqCtx).find((path => path.search(filename) > 0));
      var component = reqCtx[componentPath];
      // Then access each key:
      keys.forEach(function(k) {
        component = component[k];
      });
      component = component.default;
      return component;
    }
  }

  const fromCtx = fromRequireContext(reqCtx);
  return function(className) {
    var component;
    try {
      // `require` will raise an error if this className isn't found:
      component = fromCtx(className);
    } catch (firstErr) {
      console.error(firstErr);
    }
    return component;
  }
}
// app/javascript/entrypoints/application.jsx

import ReactRailsUJS from "react_ujs";
import { viteConstructorRequireContext } from "../helpers/viteGetConstructor";

const componentsRequireContext = import.meta.globEager("~/components/main/**/*.{js,jsx}");
ReactRailsUJS.getConstructor = viteConstructorRequireContext(componentsRequireContext);

I expect that this approach it works for you.

paul-mesnilgrente commented 1 year ago

@memoxmrdl That's awesome! I got the non-SSR pages working.

Is it going to work with SSR? I tried but nothing was displayed with no errors.

paul-mesnilgrente commented 1 year ago

Actually I found the exception:

=> #<React::ServerRendering::PrerenderError: Encountered error "#<ExecJS::ProgramError: TypeError: Cannot read properties of undefined (reading 'serverRender')>" when prerendering HelloWorld with {"name":"World"}
eval (eval at <anonymous> ((execjs):36:8), <anonymous>:6:45)
eval (eval at <anonymous> ((execjs):36:8), <anonymous>:18:13)
(execjs):36:8
(execjs):54:14
(execjs):1:40
Object.<anonymous> ((execjs):1:58)
Module._compile (node:internal/modules/cjs/loader:1103:14)
Object.Module._extensions..js (node:internal/modules/cjs/loader:1155:10)
Module.load (node:internal/modules/cjs/loader:981:32)
Function.Module._load (node:internal/modules/cjs/loader:822:12)
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/execjs-2.8.1/lib/execjs/external_runtime.rb:39:in `exec'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/execjs-2.8.1/lib/execjs/external_runtime.rb:21:in `eval'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/bundler/gems/react-rails-7814b829e645/lib/react/server_rendering/exec_js_renderer.rb:39:in `render_from_parts'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/bundler/gems/react-rails-7814b829e645/lib/react/server_rendering/exec_js_renderer.rb:20:in `render'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/bundler/gems/react-rails-7814b829e645/lib/react/server_rendering/bundle_renderer.rb:40:in `render'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/bundler/gems/react-rails-7814b829e645/lib/react/server_rendering.rb:27:in `block in render'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/connection_pool-2.4.1/lib/connection_pool.rb:110:in `block (2 levels) in with'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/connection_pool-2.4.1/lib/connection_pool.rb:109:in `handle_interrupt'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/connection_pool-2.4.1/lib/connection_pool.rb:109:in `block in with'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/connection_pool-2.4.1/lib/connection_pool[2] pry(#<React::Rails::ComponentMount>)>
ajesler-hatch commented 11 months ago

Hi!

I found a possible solution (for Vite Ruby) on this line number https://github.com/reactjs/react-rails/blob/master/react_ujs/index.js#L79 I saw that and understood how it was made the constructor so, I take the code and it change a little bit and I do the following approach:

Thanks for posting this memoxmrdl, it was super helpful. I found that if you have overlapping component names, then there is a chance it will return the wrong component. Eg searching for NewForm when you have components named NewForm and SpecialNewForm, you might get SpecialNewForm.

Our components tend to be named directory/Name.js[x] or directory/Name/index.js[x] so I've tweaked the code a little to work for our specific use case, and shared it here in case it is useful to anyone else. This isn't 100% perfect, but works for our use case and naming style. We don't use A.B to reference components, so I've removed the className splitting code.

Note that this won't work on windows as it assumes / for the path separator.

// Based on https://github.com/reactjs/react-rails/issues/1134#issuecomment-1415112288
export const viteConstructorRequireContext = function(reqCtx) {
  const componentNameMatcher = className => {
    return path => {
      return (
        path.includes(`/${className}.js`) || path.includes(`/${className}/index.js`)
      );
    };
  };

  const fromRequireContext = function(reqCtx) {
    return function(className) {
      const componentPath = Object.keys(reqCtx).find(componentNameMatcher(className));

      const component = reqCtx[componentPath];
      return component.default;
    }
  }

  const fromCtx = fromRequireContext(reqCtx);
  return function(className) {
    var component;
    try {
      // `require` will raise an error if this className isn't found:
      component = fromCtx(className);
    } catch (firstErr) {
      console.error(firstErr);
    }
    return component;
  }
}
justin808 commented 11 months ago

FWIW, I'm considering supporting Vite with https://github.com/shakacode/react_on_rails, including SSR. If anybody is interested in that, reply here, and consider our Slack Channel.

I suspect that Vite might work very easily with https://www.shakacode.com/react-on-rails-pro/, which is an easy migration from react-rails.