nebarf / module-federation-react-router-dom

Module federation example using nested routers
https://github.com/nebarf/module-federation-react-router-dom
39 stars 16 forks source link

Dynamic Remote Modules #7

Open nvonbenken opened 1 year ago

nvonbenken commented 1 year ago

How would you go about using this solution with dynamic remote modules?

I need to configure dynamic remote modules to handle deploying to different environments. Most implementations I've seen pull the remote URLs from either a static manifest or an API and import them using React.lazy. That doesn't play nicely with the use of the mount function here.

I've tried something like this but it doesn't seem to work properly. Any suggestions?

Remote app bootstrap.tsx:

import { useRef, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider, useLocation, useNavigate } from 'react-router-dom';

import { createRouter } from './app/routing/router-factory';
import { RoutingStrategy } from './app/routing/types';

const mount = ({
  mountPoint,
  initialPathname,
  routingStrategy,
}: {
  mountPoint: HTMLDivElement;
  initialPathname?: string;
  routingStrategy?: RoutingStrategy;
}) => {
  const router = createRouter({ strategy: routingStrategy, initialPathname });
  const root = createRoot(mountPoint);
  root.render(<RouterProvider router={router} />);

  return () => queueMicrotask(() => root.unmount());
};

const RemoteApp1 = ({ initialPathname }: any) => {
  const wrapperRef = useRef<HTMLDivElement>(null);
  const navigate = useNavigate();
  const location = useLocation();

  useEffect(() => {
    const remoteAppNavigationEventHandler = (event: Event) => {
      const pathname = (event as CustomEvent<string>).detail;
      const newPathname = `${initialPathname}${pathname}`;
      if (newPathname === location.pathname) {
        return;
      }
      navigate(newPathname);
    };
    window.addEventListener(
      '[RA1] navigated',
      remoteAppNavigationEventHandler 
    );

    return () => {
      window.removeEventListener(
        '[RA1] navigated',
        remoteAppNavigationEventHandler 
      );
    };
  }, [location]);

  useEffect(() => {
    if (location.pathname.startsWith(initialPathname)) {
      window.dispatchEvent(
        new CustomEvent('[host] navigated', {
          detail: location.pathname.replace(initialPathname, ''),
        })
      );
    }
  }, [location]);

  const isFirstRunRef = useRef(true);
  const unmountRef = useRef(() => {});

  useEffect(() => {
    if (!isFirstRunRef.current) {
      return;
    }
    unmountRef.current = mount({
      mountPoint: wrapperRef.current!,
      initialPathname: location.pathname.replace(initialPathname, ''),
    });
    isFirstRunRef.current = false;
  }, [location]);

  useEffect(() => unmountRef.current, []);

  return <div ref={wrapperRef} id="remote-app-1" />;
};

export default RemoteApp1;

Host component RemoteApp1.tsx:

import { loadRemoteModule } from './load-remote-module';
import { lazy, Suspense } from 'react';

import { REMOTE_APP_1_ROUTING_PREFIX } from '../routing/constants';

const remoteApp1Basename= `/${REMOTE_APP_1_ROUTING_PREFIX}`;

const RemoteApp1Module = lazy(() =>
  loadRemoteModule('remote-app-1', './Module')
);

const RemoteApp1 = () => {
  return (
    <Suspense>
      <RemoteApp1Module initialPathname={remoteApp1Basename} />
    </Suspense>
  );
};

export default RemoteApp1 ;