skip to Main Content

I’ve created simple react App HelloComponent and try to insert into Vue application but i’m unsuccessful

// React webpack config
const { ModuleFederationPlugin } = require('webpack').container;
const { merge } = require('webpack-merge');
const commonConfig = require('./webpack.common');
const path = require('path');
const devConfig = {
    mode: "development",
    entry: './src/index.jsx',
    devServer: {
        port: "5000",
    },
    plugins: [
        new ModuleFederationPlugin({
            name: 'HelloApp',
            filename: "remoteEntry.js",
            exposes: {
                './HelloComponent': './src/components/HelloComponent',
            },
            shared: { react: { singleton: true, eager: true }, "react-dom": { singleton: true, eager: true } },
        })
    ],

}
module.exports = merge(commonConfig, devConfig);
// Vue webpack Config
const HtmlWebPackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require('webpack').container;
const { VueLoaderPlugin } = require("vue-loader");
const { TransformAsyncModulesPlugin } = require("transform-async-modules-webpack-plugin");


module.exports = {
    entry: './src/bootstrap.js',
    output: {
        publicPath: "http://localhost:3002/",
    },

    resolve: {
        extensions: [".jsx", ".js", ".json"],
    },

    devServer: {
        port: 5001,
    },
    externals: {
        react: 'react', // This tells Vue to use the React instance from the remote container
        'react-dom': 'react-dom',
    },
    module: {
        rules: [
            {
                test: /.m?js/,
                type: "javascript/auto",
                resolve: {
                    fullySpecified: false,
                },
            },
            {
                test: /.vue$/,
                loader: "vue-loader",
            },
            {
                test: /.css$/i,
                use: ["style-loader", "css-loader"],
            },
            {
                test: /.(js|jsx)$/,
                exclude: /node_modules/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: ['@babel/preset-env'],
                    },
                },

            },
            {
                test: /.(jpe?g|png|gif|svg)$/i,
                loader: 'file-loader',
                options: {
                    name: 'src/assets/[name].[ext]'
                }
            }
        ],
    },

    plugins: [
        new TransformAsyncModulesPlugin(),
        new VueLoaderPlugin(),
        new ModuleFederationPlugin({
            name: "Vue App",
            filename: "remoteEntry.js",
            remotes: {
                helloApp: 'HelloApp@http://localhost:3000/remoteEntry.js',
            },
            exposes: {},
            shared: { react: { singleton: true, eager: true }, "react-dom": { singleton: true, eager: true } },

        }),
        new HtmlWebPackPlugin({
            template: "./public/index.html",
        }),
    ],
};

//App.vue

<template>
  
    <HelloComponent :name="name" />

</template>

<script>
// Importing HelloComponent from remote React application
const HelloComponent = () => import("HelloApp/HelloComponent");

export default {
  name: "App",
  data() {
    return {
      name: "World",
    };
  },
  components: {
    HelloComponent,
  },
};
</script>

My react App working as expected but Vue not able to render, Can someone help on this issue

I try for different way of insert into Vue but it’s not working. I’m little new in the Vue, Your response will be highly appriciated or Please suggest me any other approach, which will render react app in Vue

React version 18
and Vue version 3

Please suggest me any other solutions which is using webpack 5 and module federation

React

{
  "name": "base-app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@babel/core": "^7.24.4",
    "@babel/preset-env": "^7.24.4",
    "@babel/preset-react": "^7.24.1",
    "@testing-library/jest-dom": "^5.17.0",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "axios": "^1.6.8",
    "babel-loader": "^9.1.3",
    "html-webpack-plugin": "^5.6.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "terser-webpack-plugin": "^5.3.10",
    "web-vitals": "^2.1.4",
    "webpack": "^5.91.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^4.15.2",
    "webpack-merge": "^5.10.0"
  },
  "scripts": {
    "start": "webpack serve --config webpack.dev.js",
    "build": "webpack build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Vue

{
  "name": "vue-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "start": "webpack-dev-server  --open --mode development",
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.8.3",
    "vue": "^3.2.13"
  },
  "devDependencies": {
    "@babel/core": "^7.24.4",
    "@babel/eslint-parser": "^7.12.16",
    "@babel/plugin-transform-runtime": "^7.24.3",
    "@babel/preset-env": "^7.24.4",
    "@vue/cli-plugin-babel": "~5.0.0",
    "@vue/cli-plugin-eslint": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
    "@vue/compiler-sfc": "^3.4.25",
    "babel-loader": "^9.1.3",
    "css-loader": "^7.1.1",
    "eslint": "^7.32.0",
    "eslint-plugin-vue": "^8.0.3",
    "file-loader": "^6.2.0",
    "html-webpack-plugin": "^5.6.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "style-loader": "^4.0.0",
    "transform-async-modules-webpack-plugin": "^1.1.0",
    "vue-loader": "^17.4.2",
    "vue-template-compiler": "^2.7.16",
    "webpack": "^5.91.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^5.0.4"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/vue3-essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "@babel/eslint-parser"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead",
    "not ie 11"
  ]
}


Vue Error

2

Answers


  1. Webpack Module federation might also work, but there is a dedicated library that does exactly this.

    Have a look here: https://github.com/devilwjp/veaury

    You can use it with webpack,vite or vue cli

    Hopefully, that does solve your problem.

    Login or Signup to reply.
  2. I’ve created a monster for you.

    Summary

    Using React, I’ve crafted two example components named Counter and Input. I’ve also developed a function that creates a new Vue component and injects the provided React component into that Vue component.

    It’s crucial that each initialization has a unique ID for the div where we insert the React component. Additionally, it’s important not to insert the React element into a div affected by reactive Vue parameters. This is why I had to insert a child div and reference it when invoking ReactDOM.

    let reactComponentCount = 0;
    function getComponentFromReact(ReactComponent) {  
      return {
        template: `
          <div :id="reactComponentId">
            <div></div>
          </div>
        `,
        setup() {
          // Generate New Id for every created vue element
          const reactComponentId = `react-component-${reactComponentCount++}`;
          
          onMounted(() => {
            ReactDOM.render(<ReactComponent />, document.getElementById(reactComponentId).firstChild);
          });
          
          onBeforeUnmount(() => {
            ReactDOM.unmountComponentAtNode(document.getElementById(reactComponentId).firstChild);
          });
          
          return {
            reactComponentId,
          }
        },
      }
    }
    
    <template>
      <div>
        <name-of-component-from-react></name-of-component-from-react>
      </div>
    </template>
    
    <script>
    import { getComponentFromReact } from './path/to/your/function'
    
    function YourReactComponent () {
      // ...
    }
    
    export default {
      components: {
        'name-of-component-from-react': getComponentFromReact(YourReactComponent),
      },
    }
    </script>
    

    So, the essence of my example lies in the getComponentFromReact() function. And its usage is within the application’s components object.

    Example

    const { createApp, ref, watch, onMounted, onBeforeUnmount } = Vue;
    const { useState, useEffect } = React;
    
    /**
     * Create New Vue Component
     * - use ReactDOM and React component
     */
    
    let reactComponentCount = 0;
    function getComponentFromReact(ReactComponent) {  
      return {
        template: `
          <div :id="reactComponentId">
            <div></div>
          </div>
        `,
        setup() {
          // Generate New Id for every created vue element
          const reactComponentId = `react-component-${reactComponentCount++}`;
          
          onMounted(() => {
            ReactDOM.render(<ReactComponent />, document.getElementById(reactComponentId).firstChild);
          });
          
          onBeforeUnmount(() => {
            ReactDOM.unmountComponentAtNode(document.getElementById(reactComponentId).firstChild);
          });
          
          return {
            reactComponentId,
          }
        },
      }
    }
    
    /**
     * React Components
     */
    
    function Counter() {
      const [count, setCount] = useState(0);
      
      useEffect(() => {
        return () => {
          console.log("Counter Destroyed");
        };
      }, []);
     
      return (
        <div>
          {count}
          <button onClick={() => setCount(count + 1)}>+1 count</button>
        </div>
      );
    }
    
    function Input() {
      const [value, setValue] = useState('');
    
      const handleChange = (event) => {
        setValue(event.target.value);
      }
      
      useEffect(() => {
        return () => {
          console.log("Input Destroyed");
        };
      }, []);
     
      return (
        <div>
          {value}
          <input value={value} onChange={handleChange} />
        </div>
      );
    } 
    
    /**
     * Vue Application
     */
     
    const app = createApp({
      components: {
        "counter-from-react": getComponentFromReact(Counter),
        "input-from-react": getComponentFromReact(Input),
      },
      setup() {
        const mounted = ref(true);
        
        watch(mounted, () => console.clear()); // only need to clear the console for testing
        
        return {
          mounted,
        }
      },
    }).mount('#app');
    #app > * {
      margin: 10px 0;
    }
    <script src="https://unpkg.com/[email protected]/dist/vue.global.prod.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/react/umd/react.production.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/react-dom/umd/react-dom.production.min.js"></script>
    
    <div id="app">
      <div>
        <label>Mounted:</label>
        <input type="checkbox" v-model="mounted" />
      </div>
    
      <div v-if="mounted">
        - First Counter
        <counter-from-react></counter-from-react>
      </div>
      
      <div v-if="mounted">
        - Second Counter
        <counter-from-react></counter-from-react>
      </div>
      
      <div v-if="mounted">
        - First Input
        <input-from-react></input-from-react>
      </div>
      
      <div v-if="mounted">
        - Second Input
        <input-from-react></input-from-react>
      </div>
    </div>

    Create React Component

    So, it’s important to note that even though you import React components into Vue components, React elements can only be rendered by ReactDOM, as it possesses the necessary rendering logic. ReactDOM is capable of injecting the result into a native div, and by declaring this div as a Vue component, you can pass it to any Vue element as a "component".

    Destroy React Component

    Upon destruction of the Vue component, it’s essential to ensure that the React component is also properly notified of the event. It’s advisable to destroy the React component before our Vue component is destroyed. Therefore, within the onBeforeUnmount() hook, I invoke the destruction of the respective React component (which will be confirmed by a console.log message).

    Following this, the Vue component is automatically destroyed. I’ve illustrated this with an example using v-if, which can be managed by a boolean reactive variable.

    If v-if is true, the Vue element mounts and creates the React element. If v-if becomes false, not only does the component disappear, but it’s also entirely destroyed.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search