Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple instances of singleton shared dependencies are fetched and ran in runtime - "Cannot read properties of null (reading 'useEffect')" #3170

Closed
5 tasks done
danhorvath opened this issue Nov 4, 2024 · 20 comments

Comments

@danhorvath
Copy link

Describe the bug

When a host app imports a remote directly without lazy loading, in certain scenarios the shared singleton dependencies (in our case React) get downloaded for both the host and the remote, they can end up each using their own respective versions of that shared dependency.

After some investigation, we think the issue happens when the remote entry files finishes downloading before the host dependencies - see the example below, the host depends on react 18.2.0, the remote on react 18.3.1.

image
image
image

In cases where the react 18.2.0 chunk ends up finishing loading first, we do not see this issue:
image
image

Reproduction steps:

run pnpm i
run pnpm --filter=host dev
run pnpm --filter=remote dev
Expected: page loads correctly
Actual: page fails to load due to incorrect request for the deps of remote app.

The example config is (using the latest versions from @rsbuild/core, @rsbuild/plugin-react and @module-federation/rsbuild-plugin):

Host

import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';

export default defineConfig({
  server: {
    port: 3000,
  },
  plugins: [
    pluginReact(),
    pluginModuleFederation({
      name: 'host',
      remotes: {
        remote: 'remote@http://localhost:3001/foo/remoteEntry.js',
      },
      shared: {
        react: {
          singleton: true,
        },
        'react-dom': {
          singleton: true,
        },
      },
      dts: false,
      shareStrategy: 'loaded-first',
    }),
  ],
});

Remote

import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';

export default defineConfig({
  server: {
    base: '/foo',
    port: 3001,
  },
  plugins: [
    pluginReact(),
    pluginModuleFederation({
      name: 'remote',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/Button',
      },
      shared: {
        react: {
          singleton: true,
        },
        'react-dom': {
          singleton: true,
        },
      },
      dts: false,
      shareStrategy: 'loaded-first',
    }),
  ],
});

Reproduction

https://github.com/danhorvath/rsbuild-demo

Used Package Manager

pnpm

System Info

System:
    OS: macOS 15.1
    CPU: (12) arm64 Apple M3 Pro
    Memory: 1.42 GB / 36.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.17.0 - ~/.nvm/versions/node/v20.17.0/bin/node
    npm: 10.8.2 - ~/.nvm/versions/node/v20.17.0/bin/npm
    pnpm: 9.12.3 - ~/.nvm/versions/node/v20.17.0/bin/pnpm
  Browsers:
    Chrome: 130.0.6723.92
    Edge: 130.0.2849.56
    Safari: 18.1

Validations

@ScriptedAlchemy
Copy link
Member

What if you set it to version first for the strategy

@ScriptedAlchemy
Copy link
Member

React plugin takes options. Ensure router and runtime are set to false. Inspect types to see exactly what to pass.

@danhorvath
Copy link
Author

I belive this issue is not specific to react. We had very similar problems with an internal package that we use as a shared dependency, which relies it being a singleton - it exports an instance of a class that needs to be initialized, and occasionally the microfrontend causes a runtime error when it loads a different, un-initialized instance.

@danhorvath
Copy link
Author

What if you set it to version first for the strategy

We had that setting when we ran into this issue for the first time - we later changed to loaded-first hoping that it would fix it but unfortunately it didn't.

@ScriptedAlchemy
Copy link
Member

Do you use any dynamic remotes that are not import() or import from?

@ScriptedAlchemy
Copy link
Member

Okay so doing this: danhorvath/rsbuild-demo#1

I have added window.loaction.reload() to the remote, this will refresh the page perpetually till a hook error happens then stop since useEffect cant be called.

With version-first, ensuring i share react/(jsx-dev-runtime|jsx-runtime), and setting the packages to ^ and not pinning them - it i am not able to make it throw an error. If you can use that PR, edit it and make it crash we can take a look further, but i left it for a few min (about 3000 page reloads) without error, soon as i remove version first, within ~60 reloads it would crash depending on what loaded first and causing a missmatch in version.

@ScriptedAlchemy
Copy link
Member

even without

"react/": {
singleton: true,
},

or the ^ on the react and react dom in the package json, it still seems to work - as long as version-first is set.

@2heal1
Copy link
Member

2heal1 commented Nov 5, 2024

I miss to consider the shared is loading ... Okay i fix it on this pr #3176

@danantal
Copy link

danantal commented Nov 5, 2024

Do you use any dynamic remotes that are not import() or import from?

If what you mean by dynamic remotes is a runtime generated remote url, like import(runtimeUrl + "/mf-manifest.json") then the answer is no, we don't. I'm not sure of other possibilities in which we can consume a remote (except maybe if you mean require?)

The fix that @2heal1 provided should work for the exact configuration with loaded-first strategy, so really appreciate that - we can probably close this issue afterwards.

However, do you happen to have any intuition on how this could have possibly happened when running with version-first? One thing worth mentioning though was that at the time we were running with the rs-build built-in module federation configuration - which we understood is actually module federation 1.5 (since it's using @module-federation/[email protected]) and not 2.0.

Unfortunately we were not able to reproduce it easily in a simple repro like the one @danhorvath linked in this issue (it was in our real setup that uses 4 remotes all which had shared dependencies in different versions).

@ScriptedAlchemy
Copy link
Member

You're using v2.
With version first it should not happen. But without a repo to test it on its virtually impossible to try and debug.
Publish some fake packages or use a monorepo etc and you should be able to recreate a sample.

Otherwise send me an NDA and internal repo.

@danantal
Copy link

danantal commented Nov 5, 2024

I think it's fine, we are not going to downgrade regardless and we'll upgrade the rsbuild plugin to v0.7.0.

It was more of a curiosity rather than a real issue.

Thanks again for the quick reaction and fix on this 🎉

@ScriptedAlchemy
Copy link
Member

Ah, you're on an older version of RSBuild as a whole. Then yes, it may be a bug in the runtime itself that's been resolved since.

If you experience it on the latest versions, let us know and, if possible, make a reproducible sample that we can look at.

@danhorvath
Copy link
Author

Ah, you're on an older version of RSBuild as a whole.

We were on the latest versions at the time of writing.

There is one aspect of our production setup that we couldn't model well in the repro, which is that in our setup the chunks (including the shared dependencies) can be cached by the browser, but the remoteEntry is not, so often on the initial page load the remote entry is fetched faster than the shared dependencies of the host, but on subsequent loads most (usually all) shared dependencies are loaded from the disk.

I wonder if this is the reason why we experienced the same (or at least very similar) runtime error with the version-first policy too. I will try to adjust the repro with the caching mechanism to check this.

But in any case, thank you both for the quick help, it is very much appreciated. We'll verify tomorrow if the latest version resolves our issue.

@ScriptedAlchemy
Copy link
Member

ScriptedAlchemy commented Nov 5, 2024

Do you have a prod url? If we can repro it in prod, we can possibly place breakpoints there and try to diagnose it.

It may be the reason. But version first in general seems to be far more reliable than loaded first. Loaded first is the old v1 mechanism which is less deterministic. You may also want to consider using the json protocol instead of the js remotes instead. Since it is also more robust as well as we can compute the tree before any evaluation takes place.

@danhorvath
Copy link
Author

Do you have a prod url?

We rolled back to use our old webpack setup (with the old module federation) when we noticed this issue so I can't provide a url.

It may be the reason. But version first in general seems to be far more reliable than loaded first. Loaded first is the old v1 mechanism which is less deterministic. You may also want to consider using the json protocol instead of the js remotes instead. Since it is also more robust as well as we can compute the tree before any evaluation takes place.

That's good to know. We plan to migrate to using the manifest.json as soon as we stabilized our rsbuild setup. We've been working with the loaded-first strategy mainly to avoid the runtime error (as suggested here #2898 (comment)) and for the performance benefits. Thank you for the suggestion, we'll check if using version-first + ‘manifest.json‘ is stable in our case.

@ScriptedAlchemy
Copy link
Member

manifest.json was also designed to be used to control MF at the server side. If you use the json protocol you can render "module snapshots" which are lock files for the runtime - the runtime will never change behavior when a a module snapshot is present, this is how we orchestrate and control our several thousands of remotes at Bytedance.

DM me if you want a reference of it, we do not readily supply one since it requires additional infra for user to leverage it.

@danhorvath
Copy link
Author

manifest.json was also designed to be used to control MF at the server side. If you use the json protocol you can render "module snapshots" which are lock files for the runtime - the runtime will never change behavior when a a module snapshot is present, this is how we orchestrate and control our several thousands of remotes at Bytedance.

Interesting, we'll consider that option, thanks!

Regarding this issue - we verified the fix on our end and we don't see the runtime error anymore with the latest module federation runtime. Thanks you for the help!

@danhorvath
Copy link
Author

You're using v2.
With version first it should not happen. But without a repo to test it on its virtually impossible to try and debug.
Publish some fake packages or use a monorepo etc and you should be able to recreate a sample.

Following your advice, we tried switching to the version-first share strategy. Unfortunately we're facing a very similar issue. We managed to create a reproduction, but since this issue with loaded-first got fixed, we decided to open a new issue with it: #3209

@ScriptedAlchemy
Copy link
Member

Can you share the reproduction with version first. I'd still like us to investigate it!

@danhorvath
Copy link
Author

We included a link with the reproduction in the new issue that we created, it's in the same repo as above but on a branch called version-first https://github.com/danhorvath/rsbuild-demo/tree/version-first
Any help is very much appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants