2

I have a microfrontend app with the following structure. I am trying to mount both marketing and company into container but keep getting invalid hook calls. They all use react 18.2, react-dom 18.2 and react-router-dom 6.3.0. Marketing loads fine in the browser on its own but when trying to load it into container I get an invalid hook call warning. What is going on? Can I use Router in Container (host) and BrowserRouter in Marketing (remote)?

/packages
|  /company
|  |  /public
|  |  /src
|  /marketing
|  |  /public
|  |  /src
|  /container
|  |  /public
|  |  /src

Container: App.js

import React, { lazy, Suspense, useState, useEffect } from 'react';
import { Router, Route, Routes, Navigate } from 'react-router-dom';
import { createBrowserHistory } from 'history';
import { createGlobalStyle } from 'styled-components';

const GlobalStyle = createGlobalStyle`
  body {
    height: 100%;
    min-height: 100vh;
  }

  html {
    height: 100%;
  }
`

import WebFont from 'webfontloader';

WebFont.load({
  google: {
    families: ['Barlow', 'Playfair Display', 'Overpass']
  }
});

// deleted because we are now using lazy function and Suspense module
// import MarketingApp from './components/MarketingApp';
// import AuthApp from './components/AuthApp';
import Progress from './components/Navbar/Progress';
import Header from './components/Header';

const MarketingLazy = lazy(() => import('./components/MarketingApp'));
const CompaniesLazy = lazy(() => import('./components/CompanyApp'));

const history = createBrowserHistory();

export default function App() {
  // useState is a HOOK to keep track of the application state IN A FUNCTION COMPONENT, in this case if the user isSignedIn
  // State generally refers to data or properties that need to be tracking in an application
  // hooks can only be called at the top level of a component; cannot be conditional; and can only be called inside React function commponents 
  // useState accepts an initial state and returns two values: the current state, and a function that updates the state 
  const [isSignedIn, setIsSignedIn] = useState(false);

  // 
  useEffect(() => {
    if (isSignedIn) {
      history.push('/companies/last')
    }
  }, [isSignedIn]);


  return (
    <Router history={history}>
      <div>
        <GlobalStyle />
        <Header onSignOut={() => setIsSignedIn(false)} isSignedIn={isSignedIn} />
        <Suspense fallback={<Progress />} >
          <Routes>
            ...
            <Route path="/" element={ <MarketingLazy /> } />
          </Routes>
        </Suspense>
      </div>
    </Router>
  );
};

Container: MarketingApp.js imports mount function from Marketing (remote)

import { mount } from 'marketingMfe/MarketingApp';
import React, { useRef, useEffect, useContext } from 'react';
import { useLocation, UNSAFE_NavigationContext } from 'react-router-dom';

export default () => {
  const ref = useRef(null);
  let location = useLocation();
  const { navigator } = useContext(UNSAFE_NavigationContext);
  
  useEffect(() => {
    const { onParentNavigate } = mount(ref.current, {
      initialPath: location.pathname,
      onNavigate: ({ pathname: nextPathname }) => {        
        const { pathname } = navigator.location;

        if (pathname !== nextPathname) {
          navigator.push(nextPathname);
        }
      },
    });

    const unlisten = navigator.listen(onParentNavigate);

    return unlisten; // <-- cleanup listener on component unmount

  }, []);

  return <div ref={ref} />;
};

Marketing: App.js

import React from 'react';
import { Routes, Route, BrowserRouter as Router } from 'react-router-dom';

import Landing from './components/Landing';
import EarlyAccess from './components/LandingPages/EarlyAccess';

import WebFont from 'webfontloader';

if (process.env.NODE_ENV === 'development') {
  WebFont.load({
    google: {
      families: ['Barlow', 'Playfair Display', 'Overpass']
    }
  });
}

export default ({ history }) => {

  return (
    <div>
      <Router location={history.location} history={history}>
        <Routes>
          <Route exact path="/earlyaccess" element={ <EarlyAccess /> } />
          <Route exact path="/learn" element={ <EarlyAccess /> } />
          <Route path="/" element={ <Landing /> } />
        </Routes>
      </Router>
    </div>
  );
};

Marketing: bootstrap.js exports mount function when run through Container (host)

import React from 'react';
import ReactDOM from 'react-dom/client';
import { createMemoryHistory, createBrowserHistory } from 'history';

import App from './App';

// Mount function to start up the app
// NOTE: if you update mount parameters then change the code for running the app in isolation below this definition
const mount = (el, { onNavigate, defaultHistory }) => {
  // if given a default history object, assign it to history; otherwise use MemoryHistory object
  const history = defaultHistory || createMemoryHistory();

  if (onNavigate) {
    history.listen(onNavigate); // event listener tied to the history object which listens to whenever navigation occur
  }

  const root = ReactDOM.createRoot(el);

  root.render(
    <React.StrictMode>
      <App history={history} />
    </React.StrictMode>
  );

  return{
    onParentNavigate({ pathname: nextPathname }) {
      console.log('Container-marketing just navigated.');
      // console.log(location);

      const { pathname } = history.location;

      // avoid getting into an infinite loop
      if (pathname !== nextPathname) {
        history.push(nextPathname);
      }
    },
  };
};

// If we are in development and in isolation,
// call mount immediately
if (process.env.NODE_ENV === 'development') {
  const devRoot = document.querySelector('#_marketing-dev-root');

  if (devRoot) {
    mount(devRoot, { defaultHistory: createBrowserHistory() });
  }
}

// We are running through container
// and we should export the mount function
export { mount };

** Container: webpack.dev.js**

const { merge } = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const commonConfig = require('./webpack.common');
const packageJson = require('../package.json');
const path = require('path');
const globals = require('../src/variables/global')
const port = globals.port

const devConfig = {
  mode: 'development',
  output: {
    publicPath: `http://localhost:${port}/`,   // don't forget the slash at the end
  },
  devServer: {
    port: port,
    historyApiFallback: {
      index: 'index.html',
    },
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'companiesMod',
      filename: 'remoteEntry.js',
      exposes: {
        './CompaniesApp': './src/bootstrap',
      },
      shared: packageJson.dependencies,
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
  resolve: {
    // for dotenv config
    fallback: {
      "fs": false,
      "os": false,
      "path": false
    },
    extensions: ['', '.js', '.jsx', '.scss', '.eot', '.ttf', '.svg', '.woff'],
    modules: ['node_modules', 'scripts', 'images', 'fonts'],
    alias: {
      mdbReactUiKit: 'mdb-react-ui-kit'
    },
  },
};

module.exports = merge(commonConfig, devConfig);

** Marketing (remote): webpack.dev.js **

const { merge } = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const commonConfig = require('./webpack.common');
const packageJson = require('../package.json');

const devConfig = {
  mode: 'development',
  output: {
    publicPath: 'http://localhost:8081/'   // don't forget the slash at the end
  },
  devServer: {
    port: 8081,
    historyApiFallback: {
      index: 'index.html',
    },
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'marketingMod',
      filename: 'remoteEntry.js',
      exposes: {
        './MarketingApp': './src/bootstrap',
        './FooterApp': './src/components/footer/Footer',
      },
      shared: packageJson.dependencies,
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

module.exports = merge(commonConfig, devConfig);

Container: package.json

{
  "name": "containerr",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack serve --config config/webpack.dev.js",
    "build": "webpack --config config/webpack.prod.js"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/theCosmicGame/mavata.git",
    "directory": "packages/containerr"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "peerDependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.3.0"
  },
  "devDependencies": {
    "@babel/core": "^7.18.6",
    "@babel/plugin-transform-runtime": "^7.18.6",
    "@babel/preset-env": "^7.18.6",
    "@babel/preset-react": "^7.18.6",
    "@svgr/webpack": "^6.2.1",
    "babel-loader": "^8.2.5",
    "clean-webpack-plugin": "^4.0.0",
    "css-loader": "^6.7.1",
    "file-loader": "^6.2.0",
    "html-webpack-plugin": "^5.5.0",
    "sass-loader": "^13.0.2",
    "style-loader": "^3.3.1",
    "url-loader": "^4.1.1",
    "webfontloader": "^1.6.28",
    "webpack": "^5.73.0",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^4.9.3",
    "webpack-merge": "^5.8.0"
  },
  "dependencies": {
    "@emotion/react": "^11.9.3",
    "@emotion/styled": "^11.9.3",
    "@mui/icons-material": "^5.8.4",
    "@mui/material": "^5.9.0",
    "styled-components": "^5.3.5"
  }
}

** Marketing: package.json**

{
  "name": "marketing",
  "version": "1.0.0",
  "scripts": {
    "start": "webpack serve --config config/webpack.dev.js",
    "build": "webpack --config config/webpack.prod.js"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/theCosmicGame/mavata.git",
    "directory": "packages/marketingg"
  },
  "devDependencies": {
    "@babel/core": "^7.12.3",
    "@babel/plugin-transform-runtime": "^7.12.1",
    "@babel/preset-env": "^7.12.1",
    "@babel/preset-react": "^7.12.1",
    "babel-loader": "^8.1.0",
    "clean-webpack-plugin": "^3.0.0",
    "css-loader": "^5.0.0",
    "file-loader": "^6.2.0",
    "html-webpack-plugin": "^4.5.0",
    "sass-loader": "^13.0.0",
    "style-loader": "^2.0.0",
    "webfontloader": "^1.6.28",
    "webpack": "^5.4.0",
    "webpack-cli": "^4.1.0",
    "webpack-dev-server": "^3.11.0",
    "webpack-merge": "^5.2.0"
  },
  "peerDependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.3.0"
  },
  "dependencies": {
    "@emotion/react": "^11.9.3",
    "@emotion/styled": "^11.9.3",
    "@mui/material": "^5.9.0",
    "styled-components": "^5.3.5"
  }
}
melv3223
  • 67
  • 5
  • My crystal ball guess (since we don't see your bundling/externals configuration): multiple instances of React, one in each bundle. They don't share the same symbols, so mixing hooks won't work. – AKX Jul 17 '22 at 19:11
  • I added the dependencies and webpack config. How would I accomplish mounting the marketing app in container? – melv3223 Jul 18 '22 at 01:04
  • Hello, do you have the code hosted somewhere for reference ( Github ) ? This will help actually for reference. I am facing issues with react-router-dom v6. Your code will help me – SDK Jan 05 '23 at 15:50

1 Answers1

0

In my webpack.dev.server, I changed

shared: packageJson.dependencies,

to...

shared: {...packageJson.dependencies, ...packageJson.peerDependencies}

and the warning went away

melv3223
  • 67
  • 5