renchap/webpacker-react

How to migrate from this gem to the rails 7 setup?

mferrerisaza opened this issue ยท 14 comments

Hey

Since webpacker is being deprecated on Rails 7. I was wondering how could we migrate an app that uses this gem to the new rails 7 Esbuild/Webpack Setup.

Any help would be very much appreciated

I'm looking to get this working with esbuild and esm modules. There are old format modules using require in the script and seems like those need to be updated. Will look into the problem but if I don't find anything, maybe @renchap can weigh in. It would be great to get this working with Rails 7!

I would love to know as well. Looking into it and will leave a comment once I find a good answer.

This gem is tied to Webpacker, and I think I will deprecate it as well following Webpacker's deprecation.

To replace Webpacker, you can either:

  • use Shakapacker, a maintainer fork of Webpacker (but with significant changes)
  • use jsbundling-rails, which is the new "Rails" way. This does not handle Hot Module Reloading
  • switch to Vite-Ruby. It uses the Vite to power all JS/CSS work, which is simpler than Webpack and probably faster

Then you need to replace webpacker-react itself, to get the Rails helpers working. It should not be difficult to adapt its code to your project. Basically you need:

  • Rails helpers to output <div> with custom attributes containing the name & props of a React Component
  • a JS function to add components to a globally available object
  • a JS onload handler that search for the <div> outputed above and renders them using react-dom

I am wondering if this project can be easily converted to no longer rely on Webpacker at all, I will try to have a look at it but no promises!

I'm using this gem without webpacker (but without HSM).
I migrated my project from rails 6 to rails 7 with success.

I'm using jsbundling-rails with esbuild on rails 7.0.1, ruby 3.0.2

I only have to add lodash as dependency in package.json (perhaps it can be added as dependency to this module)

Perhaps this module should be renamed to suppress the reference to webpacker.
Hope this help!

I made some tests and this gem indeed does not need Webpacker at all.

I am a bit busy right now, but I will try find find time to:

  • remove all references to Webpacker
  • find a new name (any ideas?)
  • rewrite the JS part in typescript
  • cleanup the repo / build system to use more modern tools and file layouts
  • release this brand new version

I upgraded rails7 from rails 6, that choise propshaft, jsbuilding-rails.

When I was using rails6, I used webpacker-react, but I didn't choose webpack, so I don't use it now.
However, I needed the react-component, so I placed the following modified version in app/assets/javascripts/react/index.js and it worked fine.

import React from 'react'
import ReactDOM from 'react-dom'
import intersection from 'lodash/intersection'
import keys from 'lodash/keys'
import assign from 'lodash/assign'
import omit from 'lodash/omit'

const CLASS_ATTRIBUTE_NAME = 'data-react-class'
const PROPS_ATTRIBUTE_NAME = 'data-react-props'

const ReactComponent = {
  registeredComponents: {},

  render(node, component) {
    const propsJson = node.getAttribute(PROPS_ATTRIBUTE_NAME)
    const props = propsJson && JSON.parse(propsJson)

    const reactElement = React.createElement(component, props)

    ReactDOM.render(reactElement, node)
  },

  registerComponents(components) {
    const collisions = intersection(keys(this.registeredComponents), keys(components))
    if (collisions.length > 0) {
      console.error(`Following components are already registered: ${collisions}`)
    }

    assign(this.registeredComponents, omit(components, collisions))
    return true
  },

  unmountComponents() {
    const mounted = document.querySelectorAll(`[${CLASS_ATTRIBUTE_NAME}]`)
    for (let i = 0; i < mounted.length; i += 1) {
      ReactDOM.unmountComponentAtNode(mounted[i])
    }
  },

  mountComponents() {
    const { registeredComponents } = this
    const toMount = document.querySelectorAll(`[${CLASS_ATTRIBUTE_NAME}]`)

    for (let i = 0; i < toMount.length; i += 1) {
      const node = toMount[i]
      const className = node.getAttribute(CLASS_ATTRIBUTE_NAME)
      const component = registeredComponents[className]

      if (component) {
        if (node.innerHTML.length === 0) this.render(node, component)
      } else {
        console.error(`Can not render a component that has not been registered: ${className}`)
      }
    }
  },

  setup(components = {}) {
    if (typeof window.ReactComponent === 'undefined') {
      window.ReactComponent = this
    }

    window.ReactComponent.registerComponents(components)
    window.ReactComponent.mountComponents()
  }
}

export default ReactComponent
{{{import React from 'react'
import ReactDOM from 'react-dom'
import intersection from 'lodash/intersection'
import keys from 'lodash/keys'
import assign from 'lodash/assign'
import omit from 'lodash/omit'

const CLASS_ATTRIBUTE_NAME = 'data-react-class'
const PROPS_ATTRIBUTE_NAME = 'data-react-props'

const ReactComponent = {
  registeredComponents: {},

  render(node, component) {
    const propsJson = node.getAttribute(PROPS_ATTRIBUTE_NAME)
    const props = propsJson && JSON.parse(propsJson)

    const reactElement = React.createElement(component, props)

    ReactDOM.render(reactElement, node)
  },

  registerComponents(components) {
    const collisions = intersection(keys(this.registeredComponents), keys(components))
    if (collisions.length > 0) {
      console.error(`Following components are already registered: ${collisions}`)
    }

    assign(this.registeredComponents, omit(components, collisions))
    return true
  },

  unmountComponents() {
    const mounted = document.querySelectorAll(`[${CLASS_ATTRIBUTE_NAME}]`)
    for (let i = 0; i < mounted.length; i += 1) {
      ReactDOM.unmountComponentAtNode(mounted[i])
    }
  },

  mountComponents() {
    const { registeredComponents } = this
    const toMount = document.querySelectorAll(`[${CLASS_ATTRIBUTE_NAME}]`)

    for (let i = 0; i < toMount.length; i += 1) {
      const node = toMount[i]
      const className = node.getAttribute(CLASS_ATTRIBUTE_NAME)
      const component = registeredComponents[className]

      if (component) {
        if (node.innerHTML.length === 0) this.render(node, component)
      } else {
        console.error(`Can not render a component that has not been registered: ${className}`)
      }
    }
  },

  setup(components = {}) {
    if (typeof window.ReactComponent === 'undefined') {
      window.ReactComponent = this
    }

    window.ReactComponent.registerComponents(components)
    window.ReactComponent.mountComponents()
  }
}

export default ReactComponent

This file is almost equivalent to this one.

@renchap

find a new name (any ideas?)

I was going to make a gem with the above modified component, but I thought it would be better to decide this issue here, so I decided not to.
The rails component in esbuild is (js|css)bundling-rails, so I thought react-component-rails would be better.

Since react is an inseparable part of my project, I will maintain this.

Seems OK for me

Thanks @yubele for the name suggestion, you are right, lets make it simple :)

I started a PR which renames this library and adopt a more modern JS build system rather that the current monster I created.
Its not yet finished, but I plan to work on it next week.

Hey all, I've been out for a while, but thanks so much for the help and the kind responses. If there is some way I could help to move this to the finish line I've be more than happy to do so.

just a remark. Actually this gem require webacker in webpacker-react.gemspec:

spec.add_dependency "webpacker"

So bundler add this gem to the list of needed gem. As this is not needed, we can suppress it and reduce the size of the bundle.
If a user use webpacker, he add it directly to the Gemfile (It's already the Rail way).

It would be nice to release a latest version of this gem without this requirement as it is not needed.

As said above, I am working on a new version of this gem, with a new name and a much simpler setup.

You can see the work in progress in this PR: #139

This new version will no longer depend on Webpacker (or Webpack).

React 18 is out ( https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html) and change the rendering methods.

Perhaps it would be needed to test "React.version" to use the correct way (< React 18 or >= React 18) of rendering

The new beta 4 work as expected for my use.