electron-userland / electron-webpack

Scripts and configurations to compile Electron applications using webpack
https://webpack.electron.build/
903 stars 170 forks source link

Webpack dev server loads @material-ui and react differently #420

Open lispmachine opened 3 years ago

lispmachine commented 3 years ago

Steps to reproduce

  1. git clone https://github.com/electron-userland/electron-webpack-quick-start.git
  2. cd electron-webpack-quick-start
  3. yarn add react react-dom @material-ui/core
  4. Change src/index/renderer.js
    
    import * as React from 'react';
    import * as ReactDOM from 'react-dom';
    import { makeStyles } from '@material-ui/core';

const useStyles = makeStyles({ root: { background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)', height: '100vh', width: '100%', }, });

function Test() { const classes = useStyles(); return React.createElement('div', {className: classes.root}); }

ReactDOM.render(React.createElement(Test, null), document.getElementById('app'));

5. `yarn dev`

**Expected results:**

I see a window with a nice gradient.

**Actual results:**

I see a white window with following error in developers console:

Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:

  1. You might have mismatching versions of React and the renderer (such as React DOM)
  2. You might be breaking the Rules of Hooks
  3. You might have more than one copy of React in the same app See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem. at resolveDispatcher (:37681/tmp/electron-webpack-quick-start/node_modules/react/cjs/react.development.js:1476) at Object.useContext (:37681/tmp/electron-webpack-quick-start/node_modules/react/cjs/react.development.js:1484) at useTheme (:37681/tmp/electron-webpack-quick-start/node_modules/@material-ui/styles/useTheme/useTheme.js:15) at useStyles (:37681/tmp/electron-webpack-quick-start/node_modules/@material-ui/styles/makeStyles/makeStyles.js:243) at Test (webpack-internal:///./src/renderer/index.js:18) at renderWithHooks (webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js:14985) at mountIndeterminateComponent (webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js:17811) at beginWork (webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js:19049) at HTMLUnknownElement.callCallback (webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js:3945) at Object.invokeGuardedCallbackDev (webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js:3994)

As you may see in a stack trace React is first loaded via webpack dev server (webpack-internal://), but @material-ui and its dependencies are loaded via direct file access (file://). This leads to 2 copies of React being used in the same app (case 3 in error message).

I suspect it is caused by alias from @ to sourceDir.

Workaround

So far I have managed to circumvent this issue by using webpack alias without @ in the name.

package.json

  "electronWebpack": {
    "renderer": {
      "webpackConfig": "renderer.webpack.js"
    }
  }

renderer.webpack.js

const path = require('path');

module.exports = {
  resolve: {
    alias: {
      'material-ui': path.resolve('./node_modules/@material-ui'),
    }
  }
};

src/rendered/index.js

import { makeStyles } from 'material-ui/core';
lispmachine commented 3 years ago

You may find sample code here: https://github.com/lispmachine/electron-webpack-quick-start/tree/material-ui-bug https://github.com/lispmachine/electron-webpack-quick-start/tree/material-ui-workaround

loopmode commented 3 years ago

This is not related to an import starting with @. It is due to webpack externals and what's called whitelisting in electron-webpack.

You can read a lot about it if you search he issues here. There is also documentation at https://webpack.electron.build/configuration#white-listing-externals

My recommendation: In your renderer webpack config, export a function instead of an object. It's the only way to have 100% control of the config and avoid merging with the defaults. In that function, you receive the current config with defaults and return the final config after mutation. Before you return, set externals to empty array.

module.exports = function (config) {
  config.externals = [];
  return config;
}
loopmode commented 3 years ago

Furthermore, the problem at its core is that you need to have exactly one copy of react in your project. Otherwise you run into exactly your problem. However, when your dependencies are all specified as webpack externals, which is the default behavior with electron-webpack, then material-ui will bring its own copy of its react dependency. You have two copies of react and it goes boom. Same happens with most other libraries that have a dependency on react.

lispmachine commented 3 years ago

This is not related to an import starting with @. It is due to webpack externals and what's called whitelisting in electron-webpack.

You can read a lot about it if you search he issues here. There is also documentation at https://webpack.electron.build/configuration#white-listing-externals

My recommendation: In your renderer webpack config, export a function instead of an object. It's the only way to have 100% control of the config and avoid merging with the defaults. In that function, you receive the current config with defaults and return the final config after mutation. Before you return, set externals to empty array.

module.exports = function (config) {
  config.externals = [];
  return config;
}

Yes this fixes this issue, as well as

module.exports = function (config) {
config.externals.push('react');
return config;
}

or the other way around

  "electronWebpack": {
    "whiteListedModules": ["@material-ui/core"],  
  }

in package.json.

Is there a reason for react to be whitelisted by default?

loopmode commented 3 years ago

Hehe. Well... It's complicated, and needlessly so :) Electron-webpack has a hard-coded feature that puts all package names found in the package.json dependencies (not devDependencies!) to the webpack externals. I guess because doing that seemed to be a recommended best practice when working with webpack and electron. You can read up on webpack externals in their docs, there's a lot info online regarding externals and electron too.

"Whitelisting" in electron-webpack basically means "do not automatically put this package to the webpack externals". React, for obvious reasons as you too found out, cannot be treated as an external in electron apps when using webpack, instead it does need to be managed by webpack and placed into the final bundle, and deduplicated too. Any lib that needs it should load the same react and react-dom.

So at some point, one collaborator added react as default, because that too seemed to make sense (else you always had to whitelist it).

My stance on this is: bollocks! Let's make a major update with a breaking change and remove the externals/whitelisting feature altogether. It was a good intention to have some magic that leads users down the right path, but ultimately it takes away important responsibility from users. Plus it locks us into this very situation (there's literally dozens of issues about this, and to users it's never obvious what the problem is. It's always "lib xy doesn't work" but the same core issue/feature.)

The only stuff I actually ever needed to be treated as external was stuff I needed in the main process. Never ever had it been anything from the renderer process. And that makes sense - why shouldn't the renderer just run any bundle as if it was an actual web page. No externals needed, put it all in the bundle.

On main process, using stuff like sqlite or some native modules etc, there it makes sense to not try squeeze it into a bundle the same way as with a website. But users who need that will find it out and manually add lib xy to webpack externals. I mean, if you'd run into this problem, you'd be doing sophisticated stuff already and would probably understand a bit about coding, webpack, electron etc. It would be much better than e.g. newcomers (or veterans) who just wanna do a hello world or todo app run into this kind of issue right away.

xucongli1989 commented 3 years ago

This is not related to an import starting with @. It is due to webpack externals and what's called whitelisting in electron-webpack.

You can read a lot about it if you search he issues here. There is also documentation at https://webpack.electron.build/configuration#white-listing-externals

My recommendation: In your renderer webpack config, export a function instead of an object. It's the only way to have 100% control of the config and avoid merging with the defaults. In that function, you receive the current config with defaults and return the final config after mutation. Before you return, set externals to empty array.

module.exports = function (config) {
  config.externals = [];
  return config;
}

I have the same problem, this works for me, thank you very much!