4

I've created simple React 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 appreciated or Please suggest me any other approach, which will render react app in Vue

React 18 and Vue 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

6
  • 1
    You can't expect Vue to render React component. Only Vue components are considered components by the framework, anything else is random piece of code. You should render React component the same way as you would do anywhere else Commented Apr 26, 2024 at 19:07
  • @kissu how is webpack "dead"? 25 million weekly downloads is hardly dead. Commented Apr 30, 2024 at 13:18
  • @kissu updated package.json for both app Commented Apr 30, 2024 at 14:41
  • 1
    React and Vue are alternatives to each other. They're not intended to be used together. Commented May 2, 2024 at 17:50
  • I don't think these two are meant to work that way. Can you tell us what the aim is for this project? Commented May 6, 2024 at 7:44

2 Answers 2

0

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.

Sign up to request clarification or add additional context in comments.

Comments

0

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.

function getComponentFromReact(ReactComponent) {
  return {
    template: `
      <div :id="reactComponentId">
        <div></div>
      </div>
    `,
    setup(props, { attrs }) {
      // Generate New Id for every created vue element
      const reactComponentId = `react-component-${reactComponentCount++}`;

      onMounted(() => {
        ReactDOM.render(
          // In Vue, all attributes (props and emits) can be queried in the 'attrs' object, and this can be passed directly to the React component
          <ReactComponent {...attrs} />,
          document.getElementById(reactComponentId).firstChild
        );
      });

      onBeforeUnmount(() => {
        ReactDOM.unmountComponentAtNode(document.getElementById(reactComponentId).firstChild);
      });

      return {
        reactComponentId,
      };
    },
  };
}
<template>
  <div>
    <name-of-component-from-react
      attributeName="value"
      anotherAttributeName="value"
    ></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(props, { attrs }) {
      // Generate New Id for every created vue element
      const reactComponentId = `react-component-${reactComponentCount++}`;

      onMounted(() => {
        ReactDOM.render(
          // In Vue, all attributes (props and emits) can be queried in the 'attrs' object, and this can be passed directly to the React component
          <ReactComponent {...attrs} />,
          document.getElementById(reactComponentId).firstChild
        );
      });

      onBeforeUnmount(() => {
        ReactDOM.unmountComponentAtNode(document.getElementById(reactComponentId).firstChild);
      });

      return {
        reactComponentId,
      };
    },
  };
}

/**
 * React Components
 */

function Counter({ start = 0, onChanged }) {
  const [count, setCount] = useState(start);
  
  const handleIncrement = () => {
    const newNumber = count + 1
    setCount(newNumber);
    if (onChanged) {
      onChanged(newNumber); // call @changed emit
    }
  };
  
  useEffect(() => {
    return () => {
      console.log("Counter Destroyed");
    };
  }, []);
 
  return (
    <div>
      {count}
      <button onClick={handleIncrement}>+1 count</button>
    </div>
  );
}

function Input({ text, onHasNumber }) {
  const [value, setValue] = useState(text);

  const handleChange = (event) => {
    const newText = event.target.value
    setValue(newText);
    if (onHasNumber) {
      const hasNumber = /[0-9]/.test(newText);
      onHasNumber(hasNumber); // call @has-number emit
    }
  }
  
  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 :start="22" @changed="(newNumber) => console.log('1. counter', newNumber)"></counter-from-react>
  </div>
  
  <div v-if="mounted">
    - Second Counter
    <counter-from-react :start="33" @changed="(newNumber) => console.log('2. counter', newNumber)"></counter-from-react>
  </div>
  
  <div v-if="mounted">
    - First Input
    <input-from-react text="first 1." @has-number="(has) => console.log('1. input', has)"></input-from-react>
  </div>
  
  <div v-if="mounted">
    - Second Input
    <input-from-react text="second 2." @has-number="(has) => console.log('2. input', has)"></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.

2 Comments

Hello Rozsazoltan, can you please share me the full code, are you using webpak module federation
Follow module-federation: React in Vue demo application for Webpack.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.