module-federation-react-router-dom icon indicating copy to clipboard operation
module-federation-react-router-dom copied to clipboard

Dynamic Remote Modules

Open nvonbenken opened this issue 2 years ago • 0 comments

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 ;

nvonbenken avatar Apr 27 '23 14:04 nvonbenken