Skip to content

Conversation

@ScriptedAlchemy
Copy link
Member

@ScriptedAlchemy ScriptedAlchemy commented Oct 30, 2025

Summary

This PR implements stable rerender behavior in the bridge so remote apps are NOT recreated on host prop changes. It adds an optional rerender hook for conditional recreation, and ensures custom render(App, container) implementations interop correctly.

Fixes #4171

Problem

Previously, each host rerender rebuilt different wrapper component types inside the bridge. React treated these as new elements and unmounted/remounted the remote tree, causing:

  • Lost component state on every prop change
  • Unnecessary work and flicker
  • Confusion for custom render() integrations

What Changed

  • Stable element identity: removed per-render wrapper definitions and build a single, stable element tree.
  • One root per DOM node: create a root exactly once; subsequent updates call root.render().
  • Optional rerender control: rerender(info) may return { shouldRecreate: true } to explicitly recreate a root; otherwise props update in place.
  • Custom render interop: a user-provided render(App, dom) is called on initial mount only; the returned root is reused for updates.
  • Lifecycle emits on recreation: beforeBridgeDestroy and afterBridgeDestroy fire during explicit recreation.

API

createBridgeComponent({
  rootComponent: App,
  // Optional: drive recreation explicitly when needed
  rerender: (info) => ({ shouldRecreate: !!info.props?.forceRecreate }),
  // Optional: custom root creation (called once on mount)
  render: (App, container) => {
    const { createRoot } = require('react-dom/client');
    const root = createRoot(container as HTMLElement);
    root.render(App);
    return root;
  },
});
  • Omit rerender to always preserve state on prop changes.
  • Provide rerender only when you truly need to rebuild (e.g., version swap, incompatible context change).

Tests

Added/extended tests in packages/bridge/bridge-react/__tests__/rerender-issue.spec.tsx:

  • Preserves state on prop changes (instance id stable)
  • Works without rerender (back compat) and with rerender returning void
  • Conditional recreation unmounts and creates a new root
  • Custom render(App, container) preserves state on updates and is invoked again only on recreation
  • Lifecycle destroy emits occur on recreation
  • beforeBridgeRender can inject extraProps into child props

Router test utility no longer hard-depends on jsdom; it falls back to Jest's jsdom environment for portability.

Backward Compatibility

  • No breaking changes. Existing consumers continue to work.
  • The new rerender hook is opt-in and defaults to preserving state.

Implementation Notes

  • Core logic lives in packages/bridge/bridge-react/src/provider/versions/bridge-base.tsx.
  • Tests live in packages/bridge/bridge-react/__tests__/rerender-issue.spec.tsx.

Maintenance

  • Warnings about “React 18 detected in legacy mode” remain intentional for the legacy entry and are unrelated to this change.

philip-lempke and others added 4 commits October 27, 2025 12:49
path.join is not meant for URLs. Code does not work in Node > 22.11 and produces an Invalid URL. Fixing by setting the origin as the URL base.
Fixes CI format check failure by adding required trailing comma.
- Add rerender option to ProviderFnParams interface for custom rerender handling
- Update bridge-base implementation to support custom rerender logic
- Add component state tracking to detect rerenders vs initial renders
- Preserve component state when shouldRecreate is false
- Maintain backward compatibility for existing code
- Add comprehensive test suite for rerender functionality

Fixes #4171
@changeset-bot
Copy link

changeset-bot bot commented Oct 30, 2025

🦋 Changeset detected

Latest commit: 2a4045b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 37 packages
Name Type
@module-federation/bridge-react Patch
@module-federation/modern-js Patch
remote5 Patch
remote6 Patch
@module-federation/runtime Patch
@module-federation/enhanced Patch
@module-federation/rspack Patch
@module-federation/webpack-bundler-runtime Patch
@module-federation/sdk Patch
@module-federation/runtime-tools Patch
@module-federation/managers Patch
@module-federation/manifest Patch
@module-federation/dts-plugin Patch
@module-federation/third-party-dts-extractor Patch
@module-federation/devtools Patch
@module-federation/bridge-vue3 Patch
@module-federation/bridge-shared Patch
@module-federation/bridge-react-webpack-plugin Patch
@module-federation/retry-plugin Patch
@module-federation/data-prefetch Patch
@module-federation/rsbuild-plugin Patch
@module-federation/error-codes Patch
@module-federation/inject-external-runtime-core-plugin Patch
@module-federation/runtime-core Patch
create-module-federation Patch
@module-federation/cli Patch
@module-federation/rspress-plugin Patch
@module-federation/metro Patch
@module-federation/metro-plugin-rnef Patch
@module-federation/metro-plugin-rnc-cli Patch
@module-federation/nextjs-mf Patch
@module-federation/node Patch
@module-federation/storybook-addon Patch
@module-federation/modernjsapp Patch
@module-federation/esbuild Patch
@module-federation/utilities Patch
website-new Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify
Copy link

netlify bot commented Oct 30, 2025

Deploy Preview for module-federation-docs ready!

Name Link
🔨 Latest commit 2a4045b
🔍 Latest deploy log https://app.netlify.com/projects/module-federation-docs/deploys/690db69af32d6000081e5aca
😎 Deploy Preview https://deploy-preview-4172--module-federation-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

- Fix shouldRecreate: true to actually unmount and recreate the root
- Implement proper root recreation with fresh React root instance
- Add comprehensive test to verify recreation behavior
- Ensure state is truly reset when shouldRecreate is true
- Maintain proper cleanup of old roots before creating new ones
- Add changeset for version bumping
- Remove test-implementation.js file (tests are in proper package location)

This addresses the issue where shouldRecreate: true was not actually
recreating the component and resetting state as promised in the API.
- Fix shouldRecreate: true to actually unmount and recreate the root
- Implement proper root recreation with fresh React root instance
- Add comprehensive test to verify recreation behavior
- Ensure state is truly reset when shouldRecreate is true
- Maintain proper cleanup of old roots before creating new ones

This addresses the issue where shouldRecreate: true was not actually
recreating the component and resetting state as promised in the API.
@ScriptedAlchemy
Copy link
Member Author

@codex review pr

- Revert packages/typescript/src/plugins/FederatedTypesPlugin.ts to main branch version
- This file was not intended to be part of the bridge-react rerender functionality PR
- Keep PR focused only on bridge-react changes for issue #4171
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ScriptedAlchemy
Copy link
Member Author

@codex address all pr comments.

@ScriptedAlchemy
Copy link
Member Author

@codex review pr

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector
Copy link

Summary

Testing

  • ⚠️ Tests not run (not requested)

View task →

@crutch12
Copy link
Contributor

crutch12 commented Nov 5, 2025

Hi @ScriptedAlchemy

Could you tell me please: how will it work with custom render implementation?

If I write something like this:

export default createBridgeComponent({
  rootComponent: App,
  
  // Custom render function
  render: (App, container) => {
    const root = createRoot(container as HTMLElement, {
      identifierPrefix: 'my-app-'
    });
    root.render(App);
    return root;
  },
});

What rerender implementation should I provide in this case?

@shevdrakon
Copy link

Hey folks! 👋

@ScriptedAlchemy,
I'm afraid suggested fix will not solve the initial problem and remote app will lose internal state no matter of rerender behavior. You can see it in 'should call custom rerender function when provided' test by checking the value of instanceId, it will be increased according to rerender count. Each rerender will UNMOUNT the remote component and mount a freashy one.

Could be verified with a small snipet in mentioned test code:

function RemoteApp({ props }: { props?: { count: number } }) {
      const instanceId = useRef(++instanceCounter);

      React.useEffect(() => {
        console.log('MOUNT');
        return () => {
          console.log('UNMOUNT');
        };
      }, []);
....
}

The core reason of losing state is laying in react reconcilation logic: component with differ type will fully replace the original one, no matter of props.

BridgeWrapper and UpdatedBridgeWrapper will get different type each render function call, hence react will unmount the previous one and mount a new one.

In case of "typical" component code the eslint will help to catch such things with the error:

ESLint: Do not define components during render. React will see a new component type on every render and destroy the entire subtree's DOM nodes and state (https://reactjs.org/docs/reconciliation.html#elements-of-different-types). Instead, move this component definition out of the parent component "XYZ" and pass data as props. (react/no-unstable-nested-components)

It's valid for bridge render function.

Potential fix:
Move BridgeWrapper definition right after the RawComponent definition and drill needed props. Type will stay the same (during rerender) and component will be updated (vs replaced) by root.render(..).

Closing thoughts:
Overall I think it should be ok to rerender the component w/o any limitations (by react nature). Not sure what problem rerender api is solving. @crutch12 by your request from 4171 I guess you have the UNMOUNT/MOUNT problem, what was the idea under the rerender api suggestion (probably I miss smth)?

- use stable element types (no nested component defs)

- create root only on first render; reuse for updates

- emit before/after destroy hooks when recreating

- honor custom render: skip re-creating root on updates
@ScriptedAlchemy
Copy link
Member Author

Addressed review feedback:

  • Preserve state on rerender: removed per-render wrapper components so React sees stable component types. The bridge now builds a stable element tree and reuses it across updates (no implicit unmount/remount).
  • Root lifecycle & recreation: only create a root once; on updates we call root.render(). When rerender returns { shouldRecreate: true }, we emit before/after destroy hooks and recreate the root (using custom render if provided).
  • Custom render interop: if a custom render(App, dom) is provided, it is invoked only on initial mount (not on every update). Rerenders reuse the returned root.
  • Tests: expanded coverage in bridge-react to assert that instance IDs remain stable during rerenders and added a case that exercises a custom render implementation.

Relevant changes:

  • packages/bridge/bridge-react/src/provider/versions/bridge-base.tsx
  • packages/bridge/bridge-react/tests/rerender-issue.spec.tsx

All bridge-react tests pass locally. Happy to adjust further if you want different naming or hook placement.

@shevdrakon
Copy link

@ScriptedAlchemy the initial problem solved by latest commits, thx a lot! :)

I still wonder what is the real use case under the recreation logic? When it could be useful?
Also found that propsStateMap is only used to set/delete the value, the code never reuses the value from the Map. Is that by intention?

@ScriptedAlchemy
Copy link
Member Author

ScriptedAlchemy commented Nov 7, 2025

Thanks all for the thoughtful feedback — I’ve addressed the concerns and expanded tests accordingly.

@shevdrakon

  • You were absolutely right about nested component definitions causing remounts. The bridge now builds a stable element tree (no per-render wrappers), and we create the root exactly once. Subsequent updates call root.render(...) with the same component types, so React preserves state.
  • Added assertions proving instance stability on rerender and a test that exercises explicit recreation via { shouldRecreate: true }.
  • Lifecycle hooks beforeBridgeDestroy/afterBridgeDestroy now emit around recreation.

@crutch12

  • Custom render(App, container) is fully supported:
    • Called once on mount; the returned root is reused for updates.
    • You don’t need to provide rerender to preserve state — omit it to update-in-place.
    • If you need a rebuild for a specific condition, use rerender: info => ({ shouldRecreate: condition }).
  • Added coverage to prove custom render() is invoked again only when we explicitly recreate.

What changed since the original submission

  • Stable component identity; no remounts on prop changes.
  • One root per DOM node; render() on updates.
  • Lifecycle destroy emits on recreation.
  • New tests covering state preservation, void rerender, conditional recreation, custom render interop, and extraProps injection.

Relevant files

  • packages/bridge/bridge-react/src/provider/versions/bridge-base.tsx
  • packages/bridge/bridge-react/__tests__/rerender-issue.spec.tsx
  • packages/bridge/bridge-react/__tests__/util.ts

Happy to tweak naming or placement if you’d prefer a different structure. Thanks again! 🙌

@ScriptedAlchemy
Copy link
Member Author

@codex review pr

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

… root has no render()

Add tests to cover fallback behavior and ensure UI updates + custom render is invoked again.
@ScriptedAlchemy
Copy link
Member Author

Thanks for the catch — addressed.

  • Added a compatibility fallback for custom render implementations that return a handle without render().
    • On updates, when the stored root has no render, the bridge now reinvokes the provided render(App, dom) and stores the new handle.
    • This mirrors the older behavior where custom render could be called for each update and lets those implementations manage their own internal update logic.
  • Tests: added a case that returns a handle without render() and verifies both that the UI updates on prop change and that the custom render is invoked again.

Files:

  • packages/bridge/bridge-react/src/provider/versions/bridge-base.tsx
  • packages/bridge/bridge-react/tests/rerender-issue.spec.tsx

If you prefer that we also emit destroy hooks in this fallback path, happy to wire that in — right now we keep it as a pure update for backward-compat parity.

@crutch12
Copy link
Contributor

crutch12 commented Nov 7, 2025

I guess you have the UNMOUNT/MOUNT problem, what was the idea under the rerender api suggestion (probably I miss smth)?

Yeah, the main problem was the UNMOUNT/MOUNT problem. So I suggested the rerender method, where we could control how to rerender our app

At first I tried to implement it by myself (without bridge-react at all), using the reference (https://module-federation.io/practice/bridge/overview.html#bridge-implementation-principles)

Here is my implementation:

// remote
export default function createProvider() {
  const rootMap = new Map();
  const listenersMap = new Map();
  const propsMap = new Map();

  const Renderer = ({ dom }) => {
    const [_, setState] = React.useState(0)

    React.useEffect(() => {
      listenersMap.set(dom, () => {
        setState(s => s + 1)
      })

      return () => listenersMap.delete(dom)
    }, [])

    const props = propsMap.get(dom)

    return <App {...props} />
  }

  return {
    // Render application to specified DOM node
    render(props, dom) {
      const root = createRoot(dom, { identifierPrefix: 'bridge' });
      rootMap.set(dom, root);
      propsMap.set(dom, props)

      root.render(
        <Renderer dom={dom} />
      );
    },

    rerender(props, dom) {
      propsMap.set(dom, props)
      const listener = listenersMap.get(dom);
      if (listener) {
        listener()
      }
    },

    // Clean up resources
    destroy(info, dom) {
      const root = rootMap.get(dom);
      root?.unmount();
      rootMap.delete(dom);
      listenersMap.delete(dom)
      propsMap.delete(dom)
    },
  }
}
// host
const RootApp = React.lazy(async () => {
  // Load remote module
  const module = await mf.loadRemote('...');
  const provider = module.default;

  return {
    default: (props) => {
      const containerRef = React.useRef(null);
      const providerRef = React.useRef(null);

      React.useEffect(() => {
        if (containerRef.current) {
          // Create provider instance
          const instance = provider();
          providerRef.current = instance;

          // Render remote application
          instance.render(props, containerRef.current);
        }

        // Cleanup function
        return () => {
          if (providerRef.current && containerRef.current) {
            providerRef.current.destroy({}, containerRef.current);
            providerRef.current = undefined
          }
        };
      }, []);

      React.useMemo(() => {
        if (providerRef.current) {
          const instance = providerRef.current

          // Rerender remote application
          instance.rerender(props, containerRef.current);
        }
      }, [props])

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

So I thought about rerender like function that forces component to rerender, thus I used useState to do this.

As I can see now, bridge-react is able to do "rerender" without useState, so my initial idea is unnecessary 🤷 (if we have fixed UNMOUNT/MOUNT problem)

So shouldRecreate is fully idea of @ScriptedAlchemy, but I think it looks great

@crutch12
Copy link
Contributor

crutch12 commented Nov 7, 2025

Honestly, I don't see cases where I will use shouldRecreate: true
I can always recreate children tree by myself, if I need to

@shevdrakon
Copy link

@crutch12 that's what I was thinking too: if I want to recrerate the tree I would use the react technique instead, like drill the unqiue key prop value. But as I understand @ScriptedAlchemy has a strong opinion to implement rerender api, guess there is a reason for that, differ form initial UNMONT topic :)

Just to reiterate: rerender seems like a separate feature that has nothing to do with the original problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@module-federation/bridge-react - support rerender functionality

5 participants