webpacker-react icon indicating copy to clipboard operation
webpacker-react copied to clipboard

v1.0.0-beta.4 is incompatible with React <18

Open rossta opened this issue 2 years ago • 3 comments

Expected Behavior

From the README:

This package tries to support both the legacy React DOM interface (ReactDOM.render) and the new one introduced in React 18 (ReactDOM.createRoot).

The installed React version is tested at runtime by trying to import react-dom/client. If the import succeeds then the new API is used, otherwise we fallback to the legacy API.

I'd expect this library to work with React 17.

Actual Behavior

During webpack compilation, I see the following error:

ERROR in ./node_modules/react-components-rails/dist/index.js 1:1677-1703
Module not found: Error: Can't resolve 'react-dom/client' in '/Users/rosskaffenberger/dev/stitchfix/fe-infra-webpack-rails/gem/spec/sample/node_modules/react-components-rails/dist'

Dependencies:

  • react-component-rails 1.0.0-beta.4
  • react 17.0.2
  • react-dom 17.0.2

Analysis

It appears that the import from "react-dom/client" statement will not work in versions of React prior to 18.

One solution I can think of is for react-component-rails to be refactored in a way such that the behavioral differences between React 18+ and React <18 can be isolated and injected via separate imports from the consumer, i.e., something like the following:

  • apps using React 18: import ReactComponentRails from "react-component-rails"
  • apps using React <18: import ReactComponentRails from "react-component-rails/legacy"

rossta avatar Sep 15 '22 16:09 rossta

Thanks for your report.

This is weird as there is no import … from "react-dom/client" but a dynamic import (await import('react-dom/client')) which should be resolved at runtime and a failure means it fallbacks to the legacy import. But I guess webpack needs to resolve this path when generating the output before it sends it to the server?

I wanted to find a way to not require users to change their imports to use the legacy React DOM API. It looks like we could use ReactDOM.createRoot as it is exported from react-dom, but the react-dom/client export has a slightly different implementation and I do not know if it is safe to use the non-client export…

renchap avatar Sep 15 '22 21:09 renchap

Yes, I misspoke about the import. It is the dynamic import that webpack can't process at compile time even though the module isn't requested until runtime.

I know react-component-rails is meant to be bundler-agnostic and, while I don't know for sure if behavior affects other bundlers, I do have a webpack-specific workaround. I can instruct webpack to load another module instead in place of react-dom/client using alias. The aliased module simply raises an error which react-component-rails gracefully handles in the dynamic import, falling back to React 17 rendering.

// ... add to existing webpack config
{
 // ...
  resolve: {
    alias: {
      'react-dom/client$': path.resolve('path/to/module/that/raises/error'),
    },
  },
};

rossta avatar Sep 17 '22 00:09 rossta

Version detection could be another option. Something like:

  private loadReactDOMClient() {
    return new Promise<void>((resolve) => {
+      const [reactMajorVersion] = React.version && React.version.split('.') || '0';
+      if (parseInt(reactMajorVersion, 10) < 18) {
+        this.#ReactDOMClient = false
+        resolve()
+      }

rossta avatar Sep 17 '22 11:09 rossta