Git Product home page Git Product logo

Comments (13)

MilanKovacic avatar MilanKovacic commented on June 25, 2024 1

Hi, great. I was busy, but now it should be easier to locate the issue.

from single-spa.

MilanKovacic avatar MilanKovacic commented on June 25, 2024

React is being loaded twice as one of the consumers matches the specific scope while the other one falls back to the global scope. Essentially, your import map is configured as: if request for these modules comes from localhost:3000, use this; otherwise, fall back to global scope.
This can create issues as react does not work correctly when there are multiple versions present on the same page. It also does not work correctly when the same version of react is loaded (consumed) multiple times.

If you remove the react and react-dom from imports, I'm getting an error
Unable to resolve bare specifier 'react' from https://cdn.jsdelivr.net/npm/@esm-bundle/[email protected]/system/react-dom.production.min.js
With the import-map without scopes all works perfec

By default react, and react-dom are externalized in webpack configurations (if created with create-single-spa). They will not be bundled and imports will be left as-is. When you remove them from import maps, browser encounters a line in the code such as "import ... from 'react'", and you get the error as it can not locate "react".

from single-spa.

ValeraKorovelkov avatar ValeraKorovelkov commented on June 25, 2024

@MilanKovacic, thank you for your responses! Could you please provide further clarification?
I currently have a root app using React 16, which I intend to load from node_modules. Additionally, there are several single-spa apps using React 17, and I want them to load React from the import map.
image
If I understand correctly, to ensure proper functionality, both the root app and the single-spa apps should have the same version of React?

I've used a create-react-app and craco(to update webpack) for my single-spa apps.

from single-spa.

MilanKovacic avatar MilanKovacic commented on June 25, 2024

This setup could work correctly. In root-config, exclude react, and react-dom from externals so that they are bundled. In microfrontends, set react, and react-dom as externals, and set them in import maps (you do not need to scope them). Best practice is for root-config not to use any framework, and to share dependencies like react accross microfrontends.

from single-spa.

ValeraKorovelkov avatar ValeraKorovelkov commented on June 25, 2024

The root app doesn't have any externals, all issues that I described were without externals in the root ap.

you do not need to scope them

We have multiple single-spa apps owned by different teams, and I aimed to use scopes as a means for each team to update their dependencies without affecting other teams' work.

Best practice is for root-config not to use any framework, and to share dependencies like react accross microfrontends.

This is our goal. Root app is a monolith legacy, we're gradually working on separating it step by step. However, this process will take some time.

from single-spa.

MilanKovacic avatar MilanKovacic commented on June 25, 2024

The root app doesn't have any externals, all issues that I described were without externals in the root.
After I open the page with single-spa-react app it loads react twice

I am not sure what the issue is. This is an expected behavior as you have one react in global scope of the import one, and one scoped react. All requests originating from https://localhost:3000/ will utilize the scoped one, while all other consumers will utilize the global one, which could lead to multiple versions/instances being loaded.

If you remove the react and react-dom from imports, I'm getting an error

This is happening because you have react/react-dom externalized in one of the applications.

We have multiple single-spa apps owned by different teams, and I aimed to use scopes as a means for each team to update their dependencies without affecting other teams' work.

You can use scopes for this. This is an organizational decision — be careful of microfrontend anarchy.

from single-spa.

ValeraKorovelkov avatar ValeraKorovelkov commented on June 25, 2024

I am not sure what the issue is. This is an expected behavior as you have one react in global scope of the import one, and one scoped react. All requests originating from https://localhost:3000/ will utilize the scoped one, while all other consumers will utilize the global one, which could lead to multiple versions/instances being loaded.

But I have only one consumer. In my example, there is one single-spa app. The root app uses react from the node_modules, so it should not consume a react from import-map. So with one single-spa react, that has external react it loads scoped and global react.

This is happening because you have react/react-dom externalized in one of the applications.

But I have scoped react.

{
      "imports": {
          "single-spa": "https://cdn.jsdelivr.net/npm/[email protected]/lib/system/single-spa.min.js",
          "@root-config": "{{MFE_JS_URL}}"
      },
      "scopes": {
          "https://localhost:3000/": {
              "react": "https://cdn.jsdelivr.net/npm/@esm-bundle/[email protected]/system/react.production.min.js",
              "react-dom": "https://cdn.jsdelivr.net/npm/@esm-bundle/[email protected]/system/react-dom.production.min.js"
          }
      }
  }

With this import-map I'm getting the error
Unable to resolve bare specifier 'react' from https://cdn.jsdelivr.net/npm/@esm-bundle/[email protected]/system/react-dom.production.min.js
Same conditions, one consumer and root that uses node_modules react.

from single-spa.

MilanKovacic avatar MilanKovacic commented on June 25, 2024

Please post full root-config index file, and webpack/craco configurations.

from single-spa.

ValeraKorovelkov avatar ValeraKorovelkov commented on June 25, 2024

Webpack config for single-spa app(i've removed jest configuration)

const singleSpaApplicationPlugin = require('craco-plugin-single-spa-application');
const orgName = 'org';
const projectName = 'singleSpa';

module.exports = {
    webpack: {
        plugins: {
            remove: ['ManifestPlugin'],
        },
        configure: {
            externals: ['react', 'react-dom']
        }
    },
    plugins: [
        {
            plugin: {
                ...singleSpaApplicationPlugin,
                overrideCracoConfig: ({ cracoConfig, pluginOptions }) => {
                    const cracoConfigOverride =
                        singleSpaApplicationPlugin.overrideCracoConfig({
                            cracoConfig,
                            pluginOptions,
                        });

                    // Don't remove HtmlWebpackPlugin for ephemerals to work
                    cracoConfigOverride.webpack.plugins.remove =
                        cracoConfigOverride.webpack.plugins.remove.filter(
                            (el) => el !== 'HtmlWebpackPlugin',
                        );

                    return cracoConfigOverride;
                },
            },
            options: {
                orgName,
                projectName,
                entry: `src/${orgName}-${projectName}`,
                orgPackagesAsExternal: false, 
                reactPackagesAsExternal: false,
                minimize: process.env.NODE_ENV === 'production', 
                rootDirectoryLevel: 1,
            },
        },
    ],
};

Root config file. We use it as a separate entry in the webpack

entry: {
    reactapp: [path.join(__dirname, 'index.jsx')],
    mfe: [path.join(__dirname, 'root-config.js')],
},

There is moreregisterApplication, but I'm testing this one so I believe others are irrelevant.

import { registerApplication, start, addErrorHandler } from 'single-spa';

window.addEventListener('single-spa:app-change', event => {
    const { appsByNewStatus } = event.detail;

    // As core styles were overwriting our styles, this is the solution we came up with:
    // Looking for core styles index
    const coreStylesIndex = [...document.styleSheets].findIndex(
        styleSheet =>
            styleSheet.href && styleSheet.href.includes('/css/app.css'),
    );

    // If it is micro FE that does not need core styles, then set it to true
    if (coreStylesIndex >= 0) {
        const isDisabled = appsByNewStatus.MOUNTED.some(mountedApp =>
            DISABLED_CORE_STYLES_APPS.includes(mountedApp),
        );

        document.styleSheets[coreStylesIndex].disabled = isDisabled;
    }
});

window.addEventListener('single-spa:app-change', event => {
    const { newAppStatuses } = event.detail;
    console.log('Some applications were mounted/unmounted: ');
    console.log(newAppStatuses);
});

window.addEventListener('single-spa:routing-event', event => {
    const { newAppStatuses } = event.detail;

    console.log(newAppStatuses); // { app1: MOUNTED, app2: NOT_MOUNTED }
});

registerApplication({
    name: WEB_APP_NAME,
    app: async () => {
        if (process.env.NODE_ENV === 'production') {
            await System.import(`${WEB_APP_NAME}/config`);
        }
        return System.import(WEB_APP_NAME);
    },
    activeWhen: location => {
        const { pathname } = location;

        return pathname.startsWith(`${prefix}/singleSpa`);
    },
    customProps: () => {
        const context = document.getElementById(
            `single-spa-application:${WEB_APP_NAME}`,
        )?.dataset;

        return {
            isEphemeralEnv: formatBool(context?.isEphemeralEnv),
            allFeatureFlags: { ...allFeatureFlags },
        };
    },
});

localStorage.setItem('devtools', 'false');

addErrorHandler(err => {
    System.delete(System.resolve(err.appOrParcelName));
});

start();

from single-spa.

MilanKovacic avatar MilanKovacic commented on June 25, 2024

Please add the index.html file.

from single-spa.

ValeraKorovelkov avatar ValeraKorovelkov commented on June 25, 2024

I've removed some sensitive data

{% load i18n %}
<!doctype html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
        <script src="//ajax.googleapis.com/ajax/libs/webfont/1.5.6/webfont.js"></script>
        <script src={{JARIS_CDN_URL}}></script>
        <script>
            WebFont.load({
                google: {
                    families: ['Montserrat']
                 },
                 typekit: {
                    id: 'pmn7pbb'
                 }
            });
        </script>
        {% block css %}
        <link rel="preconnect" href="https://fonts.gstatic.com">
        <link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
        {% endblock %}
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
        <meta name="application-name" content="HotSpot" />
        <meta name="apple-mobile-web-app-capable" content="yes" />
        <meta name="apple-mobile-web-app-status-bar-style" content="black" />
        <meta name="format-detection" content="telephone=no" />
        <meta name="format-detection" content="email=no" />
        <meta name="msapplication-config" content="images/favicons/browserconfig.xml">
        <meta name="theme-color" content="#f9f9f9">
        <meta id="show-tour-intercom" data-value="{{ SHOW_TOUR }}">
        <meta id="base-url-intercom" data-value="{{ BASE_URL}}">
        <meta id="intercom-tour-id" data-value="{{ INTERCOM_TOUR_ID }}">

        {% comment %}
        ***************************************************

        Setup our MicroFE's!!!

        ***************************************************
        {% endcomment %}

        <script src="https://cdn.jsdelivr.net/npm/[email protected]/runtime.min.js"></script>

        <meta name="importmap-type" content="systemjs-importmap" />
        <script type="systemjs-importmap">
            {
                "imports": {
                    "single-spa": "https://cdn.jsdelivr.net/npm/[email protected]/lib/system/single-spa.min.js",
                    "react-router-dom": "https://cdn.jsdelivr.net/npm/[email protected]/umd/react-router-dom.min.js",
                    "@root-config": "{{MFE_JS_URL}}"
                },
                "scopes": {
                    "https://localhost:3000/": {
                        "react": "https://cdn.jsdelivr.net/npm/@esm-bundle/[email protected]/system/react.production.min.js",
                        "react-dom": "https://cdn.jsdelivr.net/npm/@esm-bundle/[email protected]/system/react-dom.production.min.js"
                    }
                }
            }
        </script>
        <script type="systemjs-importmap" src="{{IMPORT_MAP_URL}}"></script>

        <link rel="preload" href="https://cdn.jsdelivr.net/npm/[email protected]/lib/system/single-spa.min.js" as="script" crossOrigin="anonymous">

        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/import-map-overrides.js"></script>

        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/system.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/extras/amd.min.js"></script>

        <script type="application/javascript">
            window.apiUrl = '{{ API_URL }}';
        </script>

        {% if should_mfe_wait_for_dom %}
        <script>
            window.addEventListener('DOMContentLoaded', function(){
                System.import('@root-config');
            });
        </script>
        {% else %}
        <script>
            System.import('@root-config');
        </script>
        {% endif %}
    </head>
    <body>
        <import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
        <script async src="//www.google-analytics.com/analytics.js"></script>
        <script src="{{ APP_JS_URL }}"></script>
        <script src="{{ REACTAPP_JS_URL }}" data-name="react-app" type="application/javascript"></script>
    </body>
</html>

from single-spa.

ValeraKorovelkov avatar ValeraKorovelkov commented on June 25, 2024

@MilanKovacic Hi! I managed to reproduce it with the default create-single-spa projects.
Here is the repository
https://github.com/ValeraKorovelkov/single-spa-shared-react-bug

from single-spa.

MilanKovacic avatar MilanKovacic commented on June 25, 2024

Hi, did you manage to solve the issue?

from single-spa.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.