evanw / esbuild

An extremely fast bundler for the web
https://esbuild.github.io/
MIT License
37.86k stars 1.12k forks source link

Esbuild best practice for bundling React apps with deps triggering multiple React instances?? #3419

Closed cameronelliott closed 10 months ago

cameronelliott commented 11 months ago

I am using esbuild 0.19.3

What is happening: In a simple React project that imports a library, which imports React: React is getting imported twice as per this --analyze=verbose output

c@macmini ~/D/r/w/e/whep-viewer (main)> npx esbuild --bundle --outdir=www/js  src/index.tsx --analyze=verbose --servedir=www src/index.tsx --platform=browser

 > Local:   http://127.0.0.1:8000/
 > Network: http://192.168.86.135:8000/
 > Network: http://192.168.86.134:8000/

  www/js/index.js ──────────────────────────────────────────────────── 1.2mb ─ 100.0%
   ├ node_modules/react-dom/cjs/react-dom.development.js ─────────── 926.6kb ── 77.6%
   │  └ node_modules/react-dom/index.js
   │     └ node_modules/react-dom/client.js
   │        └ src/index.tsx
   ├ ../../node_modules/react/cjs/react.development.js ────────────── 78.9kb ─── 6.6%
   │  └ ../../node_modules/react/index.js
   │     └ ../../dist/WhepViewer.js
   │        └ ../../dist/index.js
   │           └ src/index.tsx
   ├ node_modules/react/cjs/react.development.js ──────────────────── 78.9kb ─── 6.6%
   │  └ node_modules/react/index.js
   │     └ node_modules/react/cjs/react-jsx-runtime.development.js
   │        └ node_modules/react/jsx-runtime.js
   │           └ src/index.tsx

Modern React with hooks simply cannot work correctly when multiple instances are created

link explaining WHY you can't have multiple instances of React in an App

I simply want to ask what is the recommended way to resolve this with esbuild ?? (which I think is the bees-knees).

I made some progress using --external:react --external:react-dom, as it's clear React and React-dom don't even get bundled in this case. I got stuck with Uncaught Error: Dynamic require of "react-dom/client" is not supported error in the browser, but I think I might be over looking a flag to use Esm6 imports.

But before proceeding down that road, I want to make sure from your perspective, @evanw that this is the best way to go. (using external rather than bundling React with some sort of de-duplication)

What do you recommend as a method and mechanism to stop multiple-imports of React into applications which import libraries that import React (yet as PeerDependencies) so that esbuild can be used in this situation for bundling React apps that import libs that import React?

My sample project: https://github.com/cameronelliott/whip-whep-webrtc-react/commit/b872e15156c0cb738375bb682d6a7ea7427744fc

PS I also noticed others in issue #475 struggling with multiple instances of React: https://github.com/evanw/esbuild/issues/475#issuecomment-863277639 https://github.com/evanw/esbuild/issues/475#issuecomment-873415053 https://github.com/evanw/esbuild/issues/475#issuecomment-907563901 https://github.com/evanw/esbuild/issues/475#issuecomment-910175236 https://github.com/evanw/esbuild/issues/475#issuecomment-946760991 https://github.com/evanw/esbuild/issues/475#issuecomment-985957709 https://github.com/evanw/esbuild/issues/475#issuecomment-990149748 https://github.com/evanw/esbuild/issues/475#issuecomment-1034918835 https://github.com/evanw/esbuild/issues/475#issuecomment-1140867169

PPS You made a comment about how duplicate instances should not impact correctness, but it absolutely does in the case of React! https://github.com/evanw/esbuild/issues/475#issuecomment-1156911260

cameronelliott commented 11 months ago

It's worth noting how multiple users of Webpack resolved this multiple-instance vs single-instance of React problem: In Webpack, this config code is how some users resolve the problem:

// In webpack.config.js

  resolve: {
    alias: {
      react: path.resolve('node_modules/react'),
    },
  },

Which apparently causes React to only be output a single time in the bundle, rather than being output twice or more with multiple paths like node_modules/react, and then foolib/node_modules/react ... etc

https://stackoverflow.com/a/42411974/86375

hyrious commented 11 months ago

It's not always the bad case when you have multiple instances of React (which is rarely seen but the use case still exists).

Since you mentioned the alias feature in webpack, esbuild also has one. You just have to use --alias:react=react so that all importing to react will be resolved to the one in the root directory of your project.

cameronelliott commented 11 months ago

@hyrious Thank you, thank you, thank you for taking a minute to point out that option! That seems to be a great way to get past the multiple instances issue. ❤️

evanw commented 11 months ago

PS I also noticed others in issue #475 struggling with multiple instances of React: https://github.com/evanw/esbuild/issues/475#issuecomment-863277639 https://github.com/evanw/esbuild/issues/475#issuecomment-873415053 https://github.com/evanw/esbuild/issues/475#issuecomment-907563901 https://github.com/evanw/esbuild/issues/475#issuecomment-910175236 https://github.com/evanw/esbuild/issues/475#issuecomment-946760991 https://github.com/evanw/esbuild/issues/475#issuecomment-985957709 https://github.com/evanw/esbuild/issues/475#issuecomment-990149748 https://github.com/evanw/esbuild/issues/475#issuecomment-1034918835 https://github.com/evanw/esbuild/issues/475#issuecomment-1140867169

PPS You made a comment about how duplicate instances should not impact correctness, but it absolutely does in the case of React! https://github.com/evanw/esbuild/issues/475#issuecomment-1156911260

The issue thread you are linking to is completely irrelevant to your problem. It does not mean what you think it means. That issue is talking about esbuild generating multiple external imports for the same package in the same file, which always results in the JavaScript VM importing a single package. It's impossible for two import statements with the same path in the same JavaScript file to resolve to two different packages. So it's just an aesthetic/efficiency issue and has no effect on correctness.

The fundamental problem here is that you have multiple copies of the react library on the file system, which is presumably something npm did because some of your dependencies have incompatible requirements for which version of react to use. One way to fix that would be to make sure the libraries you are using all have compatible version ranges for react by upgrading/downgrading libraries as appropriate and/or working with the authors of your dependencies to upgrade their use of react.

You can try to hack away the problem using some kind of package alias but that could potentially result in broken code because you are forcing a version of react on a dependency that is outside of its explicitly-specified supported version range. So assuming the version range is there for a legitimate reason, the version of react you are forcing your dependency to use may have a different API and/or behavior than that package expects, which may result in the introduction of subtle bugs.

In any case, this problem is not esbuild's job to fix. The job of a bundling operation is to bundle the files on the file system as they are while respecting their real behavior. Since there are multiple copies of react on the file system, you will get multiple copies of react at run-time when you run that code in a real JavaScript environment like node, so it's correct for esbuild to include multiple copies of react in the bundle (since that's what happens when running that code for real in node).

evanw commented 10 months ago

I'm closing this issue because esbuild is working as intended here.