StephenGrider / mfe

51 stars 57 forks source link

BUG -- how many times to switch between different mfe's, the components are rendered #4

Open m-s4n opened 3 years ago

m-s4n commented 3 years ago

I wrote to you from udemy, there is a fatal error. 1)git clone https://github.com/StephenGrider/mfe.git

2) run mfe

3) switch between http://localhost:8080 and http://localhost:8080/auth/signin 4 or 5 times (soft switch)

4) components are rendering more than one

unfortunately: my big projects render many times and the performance decreases a lot over time.

KartheekJavvaji commented 3 years ago

@m-s4n Hey, I tried doing the same and couldn't find any re-renders. I checked it by adding a simple counter near bootstrap. How were you able to deduce that it's getting rendered more than once?

let counter = 0;

// Mount function to start up the app
const mount = (el, { onSignIn, onNavigate, defaultHistory, initialPath }) => {
  console.log('AUth getting mounted ', ++counter);
m-s4n commented 3 years ago

https://i.stack.imgur.com/t2iei.png

If you write the outputs from the logs in the component, you will get the log as follows if you switch between different micro frontends through the link tags each time.

not into bootstrap

KartheekJavvaji commented 3 years ago

Got it. Something like this would work -

have a containerDiv ref near the switch [or which ever container div you intend the sub-app to load into]

export default () => {
  const [isSignedIn, setIsSignedIn] = useState(false);
  const containerDivRef = useRef(null);

  useEffect(() => {
    if (isSignedIn) {
      history.push('/dashboard');
    }
  }, [isSignedIn]);

  return (
    <Router history={history}>
      <StylesProvider generateClassName={generateClassName}>
        <div>
          <Header
            onSignOut={() => setIsSignedIn(false)}
            isSignedIn={isSignedIn}
          />
          <div ref={containerDivRef}>
            <Suspense fallback={<Progress />}>
              <Switch>
                <Route path="/auth">
                  <AuthLazy containerDivRef={containerDivRef} onSignIn={() => setIsSignedIn(true)} />
                </Route>

and use ReactDOM to properly flush out the sub-app [I am sure we will have similar API in other frameworks/libs like ng or vue]

export default ({ onSignIn, containerDivRef }) => {
  const history = useHistory();

  useEffect(() => {
    let authAppRoot = document.querySelector('#auth-app-root');
    if (!authAppRoot) {
      authAppRoot = document.createElement('div');
      authAppRoot.id = '#auth-app-root';
      containerDivRef.current.appendChild(authAppRoot);
    }

    const { onParentNavigate } = mount(authAppRoot, {
      initialPath: history.location.pathname,
      onNavigate: ({ pathname: nextPathname }) => {
        const { pathname } = history.location;

        if (pathname !== nextPathname) {
          history.push(nextPathname);
        }
      },
      onSignIn,
    });

    history.listen(onParentNavigate);
    return () => ReactDOM.unmountComponentAtNode(authAppRoot);
  }, []);

  return null;
};

@StephenGrider it would be great if you can take a look at this problem

KartheekJavvaji commented 3 years ago

Cleaner solution -

function useReactMicroFEApp ({
  history,
  mount,
  additional
}) {
  const mountDivRef = useRef(null);
  const thisRef = useRef({ mountDiv: null }); // as ref on an element looses the reference to it, once its removed from DOM but reactDOM would still be attached to the same

  useEffect(() => {
      thisRef.current.mountDiv = mountDivRef.current;
      const { onParentNavigate } = mount(mountDivRef.current, {
      initialPath: history.location.pathname,
      onNavigate: ({ pathname: nextPathname }) => {
        const { pathname } = history.location;

        if (pathname !== nextPathname) {
          history.push(nextPathname);
        }
      },
      ...additional,
    });

    history.listen(onParentNavigate);
    return () => {
      ReactDOM.unmountComponentAtNode(thisRef.current.mountDiv);
    };
  }, []);

  return ({
    mountDivRef,
  })
}

export default ({ onSignIn }) => {
  const history = useHistory();

  const {mountDivRef} = useReactMicroFEApp({
    history,
    mount,
    additional: {
      onSignIn,
    }
  })

  return <div ref={mountDivRef} />;
};

Similar to this, we should be able to create hooks for other libraries [ng, vue, etc.] and have proper cleanup mechanisms according to their implementations.

m-s4n commented 3 years ago

Thanks @KartheekJavvaji I tried it and it works fine.

import { mount } from "auth/AuthApp";
import React, { useRef, useEffect } from "react";
import ReactDOM from "react-dom";
import { useHistory } from "react-router-dom";

function useReactMicroFEApp({ onSignIn }) {
  const history = useHistory();
  const mountDivRef = useRef(null);
  const thisRef = useRef({ mountDiv: null });

  useEffect(() => {
    thisRef.current.mountDiv = mountDivRef.current;

    const { onParentNavigate } = mount(mountDivRef.current, {
      initialPath: history.location.pathname,
      onNavigate: ({ pathname: nextPathname }) => {
        const { pathname } = history.location;

        if (pathname !== nextPathname) {
          history.push(nextPathname);
        }
      },
      onSignIn,
    });

    history.listen(onParentNavigate);
    return () => {
      /* --- clear up --- */
      ReactDOM.unmountComponentAtNode(thisRef.current.mountDiv);
    };
  }, []);

  return {
    mountDivRef,
  };
}

const AuthApp = ({ onSignIn }) => {
  const { mountDivRef } = useReactMicroFEApp({
    onSignIn,
  });

  return <div ref={mountDivRef} />;
};

export default AuthApp;