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

Reactivity lost on $state from class when said $state is consumed in another project (micro-frontend) #13234

Open
webJose opened this issue Sep 13, 2024 · 14 comments

Comments

@webJose
Copy link

webJose commented Sep 13, 2024

Describe the bug

Hello, I describe this in #13224 and in short, a $state value doesn't react when it is "foreign" to the project.

I made a reproduction repository where one can see that reactivity works within the context of the project that creates the state, while simultaneously not working in the other project.

Reproduction

Example repository

Logs

No response

System Info

System:
    OS: Windows 11 10.0.22631
    CPU: (16) x64 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
    Memory: 4.32 GB / 15.77 GB
  Binaries:
    Node: 20.16.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.22 - ~\AppData\Roaming\npm\yarn.CMD
    npm: 10.8.1 - C:\Program Files\nodejs\npm.CMD
    pnpm: 9.1.0 - ~\AppData\Local\pnpm\pnpm.EXE
  Browsers:
    Edge: Chromium (127.0.2651.98)
    Internet Explorer: 11.0.22621.3527

Severity

annoyance

@dummdidumm
Copy link
Member

My guess is that the microfrontends each come with their own Svelte runtime. That means the effects listen to sources being read inside them but never notice the sources from the other runtime, because those set the corresponding variable in runtime A, not B.

So the solution is to somehow deduplicate the Svelte runtimes. I'm not sure how to do that though, probably some Vite config Fu required.

@webJose
Copy link
Author

webJose commented Sep 13, 2024

Thanks for dropping by!

I tried doing what one would do for React: Import Svelte from a CDN and mark it external in Vite. This of course, requires that the project be compiled and a few other adjustments to the repository I shared. The end result is this:

image

As seen, the class using $state loaded and ran the initial effect, but the component could not be mounted. If not an imposition, could I ask that you help me understand if Svelte v5 can be de-duped or not?

@webJose
Copy link
Author

webJose commented Sep 13, 2024

Ok, I think I successfully set the project up for de-duplication of Svelte using unpkg.com. It is the same repository, but in the branch named JP/SvelteExternalized. The project runs fine in serve mode (npm run dev), but the end result is the same as the previously shown attempt.

Furthermore, even though I think I successfully de-duped Svelte, the problem in reactivity remains. 😢

@kwangure
Copy link
Contributor

Continuing from here. Even though both instances might be technically importing the same module now, they do not share the reference to the same value because of the isolation between processes on different ports...and thus their reactivity graphs (and everything else!) are separate.

@webJose
Copy link
Author

webJose commented Sep 13, 2024

Hello, @kwangure and thanks for dropping by. It seems that isolation in browsers is based on the context, and since this is all in the same window, same document, there should be no isolation happening. Or am I misunderstanding how isolation works? For example, there's no isolation going on with my module "@mfe/utils". I have verified that the constructor of the class only runs once. Since both MFE's can get a hold of the class instance, this can only mean it is the same instance, therefore the same module, even when "mfe1" is consuming the module in a different port.

@webJose
Copy link
Author

webJose commented Sep 13, 2024

Anyway, by extending the logic, it seems that externalizing the "svelte" module alone is not sufficient to do the trick. At this point I would absolutely loved it if core maintainers could come forward and extern their opinion on what it would take to make this scenario work.

@webJose
Copy link
Author

webJose commented Sep 13, 2024

My uneducated alternatives are, in my mind:

  1. Correct whatever needs correction so the Svelte runtime can be de-duped and all state from all MFE's use the same reactive graph.
  2. Provide an automatic or on-demand way of adding a "foreign" signal to the MFE's reactive graph.

For #2, it would be a requirement to be able to reactively work in any number of MFE's, not just "one or the other", or "home graph + 1". For it to be useful, it should work in an arbitrary number of graphs.

That's what I can think about, again, using whatever little knowledge of what's really going on here.

Thank you all in advance for the time you lend me reading this.

@kwangure
Copy link
Contributor

Okay. You're right. I assumed that from inspection but single-spa does some JSFu to scope all JS to root.

I've gone around removing things binary-search style in your repro. Removing vite-plugin-externalize-dependencies seems to restore reactivity inside root. I haven't looked into it further...but I'll leave you with things I'd test just from the top of my head:

  1. Whether the plugin is needed at all
  2. If Vite's builtin external option is satisfactory
  3. If other Vite plugins like vite-plugin-external and vite-plugin-externalize-deps dedupe without introducing issues
  4. If writing a simple 5-line plugin specific to your uses case works better (e.g. a simple resolveId hook that rewrites mfe1/node_modules/svelte to root/node_modules/svelte)
  5. PNPM or similar which symlinks the node_modules folder to a shared cache.
  6. ....hopefully you don't get here 🙂

@webJose
Copy link
Author

webJose commented Sep 14, 2024

Hello again, @kwangure. I hadn't realized that root had lost reactivity after I introduced externalization. My oversight. Thanks for pointing it out.

The plug-in vite-plugin-externalize-dependencies (or similar ones) are only needed when running Vite in serve mode. Whether or not my choice of plug-in was incorrect is largely unimportant because building and previewing the projects produce lifecycle_outside_component runtime error. I think we should not focus on the ability to run in serve mode for the time being, because after all, what is needed is that the built product functions. Let's put a pin on this item for later.

For your second point, this is not something I can assert, unfortunately. The condition "satisfactory" in this context can be defined as: Both micro-frontends end up using the same reactive graph. I don't have the skill to assert this condition and is probably the key point where I need help from savvy and experienced developers that know the Svelte code, that know how to assert this condition.

What I can say about your second point is that I performed the same process that is done to de-duplicate React and React-DOM.

Points 3, 4 and 5 all relate to running the projects in serve mode, which is unimportant as long as the built products cannot function properly, so I guess all those have a pin in them as well. 😭

So, unfortunately and very quickly, I arrived to point 6.

Summary

If people are willing to help with this, what we need is to focus on the built product: Can Vite de-duplicate Svelte when building the micro-frontends? If not, why? Once the reasons are known, I suppose the Svelte core team can decide whether or not a potential fix/change/upgrade/refactor is feasible and whether or not they are willing to pursue.

Once the built product works, we can discuss making it work while in serve mode, which probably has a different set of challenges.

@dummdidumm
Copy link
Member

My current thinking is that Vite can't properly handle this during dev mode because it transforms the imports to svelte etc so an import map - which would be solution here - has no possibility of actually kicking in. I think this needs vitejs/vite#6582 implemented. Until then it seems there's some workarounds mentioned in that issue, and/or you can add external in the rollup plugin options within Vite and rely on pnpm build + pnpm preview.

@webJose
Copy link
Author

webJose commented Sep 16, 2024

Yes, it would be great if that Vite issue could be implemented for sure for development mode. But that leaves us with the lifecycle_outside_component error when the projects are built and run. This should work, I think, since externalization has been a thing in rollup for a long time.

If time permits, can it be explored why the built de-duplicated version throws this error?

@paoloricciuti
Copy link
Member

@webJose i was just playing around with something similar (not exactly the same) and i think i kinda found a solution. You should externalise the dependencies also on your main project and rely on import maps. However note that the import maps should not be bundled.

<script type="importmap">
        {
            "imports": {
                "svelte": "https://esm.sh/svelte@next?no-bundle",
                "svelte/": "https://esm.sh/svelte@next&no-bundle/"
            }
        }
</script>

this is the import map I'm using and with this i'm able to import a component like this

import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";

var on_click = (_, count) => $.update_prop(count);
var root = $.template(`<h1> </h1> <button> </button>`, 1);

export default function _unknown_($$anchor, $$props) {
    let count = $.prop($$props, "count", 7);
    var fragment = root();
    var h1 = $.first_child(fragment);
    var text = $.child(h1);

    $.reset(h1);

    var button = $.sibling(h1, 2);

    button.__click = [on_click, count];

    var text_1 = $.child(button);

    $.reset(button);

    $.template_effect(() => {
        $.set_text(text, `Hello ${$$props.name ?? ""}`);
        $.set_text(text_1, count());
    });

    $.append($$anchor, fragment);
}

$.delegate(["click"]);

directly in the browser and hydrate an index.html page. Give this a try for your project and let me know.

@webJose
Copy link
Author

webJose commented Sep 26, 2024

Hello, @paoloricciuti and thanks for the note. I'll maybe try a thing or two out, but in all honesty, that looks very scary! Hehe. Even if I could mount a component like that, it is not sustainable. This kind of breakthrough is best consumed by core member teams. Maybe you guys can figure out what needs to be done in Svelte itself so it can be externalized while writing normal code, normal components, normal state stores.

@paoloricciuti
Copy link
Member

Hello, @paoloricciuti and thanks for the note. I'll maybe try a thing or two out, but in all honesty, that looks very scary! Hehe. Even if I could mount a component like that, it is not sustainable. This kind of breakthrough is best consumed by core member teams. Maybe you guys can figure out what needs to be done in Svelte itself so it can be externalized while writing normal code, normal components, normal state stores.

You can also host your own unbundled version of svelte if that's what scares you but the gist is the same. It needs to be the same module that is imported by both projects. And this is also exactly what you tried here

Ok, I think I successfully set the project up for de-duplication of Svelte using unpkg.com

But with a cdn that supports unbundled libraries.

We also opened an issue there to see if they can bundle it correctly in the bundled version.

I'll try to see if there's a decent way to externalize svelte but still build it with your main package so that you can use a single instance in your main project.

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