web-dev-qa-db-ja.com

反応フックで別の内部で1つのWebpackプロジェクトを使用するとエラーが発生する

create-react-appを使用してカスタマイズされた3つのreact-app-rewiredsがあり、どちらも次のパッケージの同じバージョンを使用しています。

"react": "^16.12.0",
"react-app-rewired": "^2.1.5",
"react-dom": "^16.12.0",
"react-router-dom": "^5.1.2",
"react-scripts": "3.3.0",

アプリ1、「メイン」アプリケーションは他の2つのアプリ用の非常にシンプルなシェルです。public/index.htmlは次のようになります。

<body>
    <div class="main-app"></div>
    <div class="sub-app-1"></div>
    <div class="sub-app-2"></div>
    <script src="sub-app-1/static/js/bundle.js" async></script>
    <script src="sub-app-1/static/js/0.chunk.js" async></script>
    <script src="sub-app-1/static/js/main.chunk.js" async></script>
    <script src="sub-app-2/static/js/bundle.js" async></script>
    <script src="sub-app-2/static/js/0.chunk.js" async></script>
    <script src="sub-app-2/static/js/main.chunk.js" async></script>
</body>

これは十分に機能し、3つのアプリはすべて正しくレンダリングされます。要件が少し変更され、sub-app-2などの<PictureFrame />の唯一のコンポーネントをsub-app-1に含める必要がある場合、メインプロジェクトに次のようにスタブコンポーネントを作成しました。

const NullPictureFrame = () => {
    return <div></div>;
}

export const PictureFrame = (props: Props) => {
    const forceUpdate = useForceUpdate();
    useEventListener(window, "PictureFrameComponent.Initialized" as string, () => forceUpdate());

    const PictureFrame = window.PictureFrameComponent || NullPictureFrame;
    return <PictureFrame />
}

フックの詳細は重要ではないと思いますが、スタンドアローンで実行しても機能します。 sub-app-2はこれに似た処理を行います

window.PictureFrameComponent = () => <PictureFrame />

理論的には機能するようですが、実際には次のエラーが発生します

The above error occurred in the <PictureFrame> component:
    in PictureFrame (at src/index.tsx:17)
    in Router (at src/index.tsx:16)
    in Unknown (at PictureFrames/index.tsx:21)
    in PictureFrame (at src/index.tsx:32) <--- in `main-app`

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/docs/error-boundaries.html to learn more about error boundaries. index.js:1
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/warnings/invalid-hook-call-warning.html for tips about how to debug and fix this problem.

main-appsub-app-2は2つの異なるバージョンのreactをロードしており、これがフックの使用時に問題を引き起こしています。

this github thread のアドバイスに基づいてwebpack設定を更新しようとしましたが、sub-app-2がこのアプローチを使用してreactへの参照を見つけられないようです。私のmain-app/config-overrides.jsは次のようになります:

config.resolve.alias["react"] = path.resolve(__dirname, "./node_modules/react");
config.resolve.alias["react-dom"] = path.resolve(__dirname, "./node_modules/react-dom");

私のsub-app-1/config-overrides.jsは次のようになります:

config.externals = config.externals || {};
config.externals.react = {
    root: "React",
    commonjs2: "react",
    commonjs: "react",
    AMD: "react"
};
config.externals["react-dom"] = {
    root: "ReactDOM",
    commonjs2: "react-dom",
    commonjs: "react-dom",
    AMD: "react-dom"
};

その結果、エラーは発生しませんが、sub-app-2のコードがreact/react-domとしてWebpackによって初期化されないこともありません。


編集1:明確化

  • 問題の3つのアプリは3つの完全な独立した(git)プロジェクトで、すべて同じ依存関係を使用しています。
  • プロジェクト間で許可される唯一の「依存関係」は、グローバル、イベント、Window.postMessageなどを使用することによるこの非常に疎結合です。
  • const PictureFrame = window.PictureFrameComponent || NullPictureFrame;の私の最初のアプローチはうまく機能しますなしこれまでチームが使用していたフックを使用して、上記の詳細な依存関係を非常に失うという考えを強化しました
  • 「react」、「react-dom」、およびmain-appexternalsの一部に依存するものを作成し、それらをそのようにロードする「明白な」方法があります。
6
Meberem

部分的な解決策:expose-loader webpackプラグインを使用する

expose-loadermain-appを使用すると、sub-app-1sub-app-2の両方がmain-appのライブラリを使用できるようになります

sub-app-1およびsub-app-2には、externalライブラリに依存していることを示すconfig-overrides.jsがあります

module.exports = function override(config, env) {
  config.externals = config.externals || {};
  config.externals.axios = "axios";
  config.externals.react = "React";
  config.externals["react-dom"] = "ReactDOM";
  config.externals["react-router"] = "ReactRouterDOM";

  return config;
}

main-appは、これらのライブラリを提供する責任があります。これは、<script />タグを使用してライブラリを埋め込むか、または expose-loader を使用して実現できます。config-overrides.jsは次のようになります。

module.exports = function override(config, env) {
  config.module.rules.unshift({
    test: require.resolve("react"),
    use: [
      {
        loader: "expose-loader",
        options: "React"
      }
    ]
  });
  config.module.rules.unshift({
    test: require.resolve("react-dom"),
    use: [
      {
        loader: "expose-loader",
        options: "ReactDOM"
      }
    ]
  });
  config.module.rules.unshift({
    test: /\/react-router-dom\//, // require.resolve("react-router-dom"), did not work
    use: [
      {
        loader: "expose-loader",
        options: "ReactRouterDOM"
      }
    ]
  });

  return config;
};

main-appページにアクセスすると、ウィンドウオブジェクトにReactReactDOMReactRouterDOMが表示されます。アプリが順不同で読み込まれる可能性がありますが、main-appが最初に読み込まれることを確認する必要があります。幸い、ややまっすぐな方法があります。 public/index.htmlで、参照されているsub-appsを削除します

<body>
    <div class="main-app"></div>
    <div class="sub-app-1"></div>
    <div class="sub-app-2"></div>
-    <script src="sub-app-1/static/js/bundle.js" async></script>
-    <script src="sub-app-1/static/js/0.chunk.js" async></script>
-    <script src="sub-app-1/static/js/main.chunk.js" async></script>
-    <script src="sub-app-2/static/js/bundle.js" async></script>
-    <script src="sub-app-2/static/js/0.chunk.js" async></script>
-    <script src="sub-app-2/static/js/main.chunk.js" async></script>
</body>

index.tsxでは、問題のスクリプトを動的にロードして、それらを待つことができます

// Will dynamically add the requested scripts to the page
function load(url: string) {
  return new Promise(function(resolve, reject) {
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.async = true;
    script.src = url;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

// Load Sub App 1
Promise.all([
    load(`${process.env.SUB_APP_1_URL}/static/js/bundle.js`),
    load(`${process.env.SUB_APP_1_URL}/static/js/0.chunk.js`),
    load(`${process.env.SUB_APP_1_URL}/static/js/main.chunk.js`)
]).then(() => {
    console.log("Sub App 1 has loaded");
    window.dispatchEvent(new Event("SubAppOne.Initialized"));
    const SubAppOne = window.SubAppOne;
    ReactDOM.render(<SubAppOne />, document.querySelector(".sub-app-1"))
});

// Load Sub App 2
Promise.all([
    load(`${process.env.SUB_APP_2_URL}/static/js/bundle.js`),
    load(`${process.env.SUB_APP_2_URL}/static/js/0.chunk.js`),
    load(`${process.env.SUB_APP_2_URL}/static/js/main.chunk.js`)
]).then(() => {
    console.log("Sub App 2 has loaded");
    window.dispatchEvent(new Event("PictureFrameComponent.Initialized")); // Kicks off the rerender of the Stub Component
    window.dispatchEvent(new Event("SubAppTwo.Initialized"));
    const SubAppTwo = window.SubAppTwo;
    ReactDOM.render(<SubAppTwo />, document.querySelector(".sub-app-2"))
});

これでmain-appReactのようなすべての共有依存関係を持ち、依存関係の準備ができると2つのsub-appsをロードします。 PictureFrameComponentがフックで期待どおりに機能するようになりました。 sub-app-1の前にmain-appをロードできますが、main-appの重要性と、PictureFrameComponentによって提供される機能のマイナーな追加を考えると、これは、一部のプロジェクトではまずまずの解決策になる可能性があります。

0
Meberem