Search code examples
reactjslazy-loadingreact-suspense

How to cache a react lazy loaded component and prevent unnecessary unmounts and remounts


I have created a lodable component,

const componentsMap = new Map();
const useCachedLazy = (importFunc, namedExport) => {
if (!componentsMap.has(importFunc)) {
    const LazyComponent = lazy(() =>
      importFunc().then(module => ({
        default: namedExport ? module[namedExport] : module.default,
      }))
    );
    componentsMap.set(importFunc, LazyComponent);
  }

  return componentsMap.get(importFunc);
};

const loadable = (importFunc, { namedExport = null } = {}) => {
  const MemoizedLazyComponent = React.memo(props => {
    const LazyComponent = useCachedLazy(importFunc, namedExport);
    return (
        <LazyComponent {...props} />
    );
  });

  return MemoizedLazyComponent;
};


export default loadable;

You can see that I have tried caching the lazy component here using memo and useCachedLazy.

My routes are called in App.js like this:

<Suspense fallback={<div><Spin /></div>}>
                  <Switch>
                    {routes.map(route => {
                      const LazyComponent = loadable(
                        () => Promise.resolve({ default: route.component }),
                        { namedExport: route.namedExport } // passing named exports to loadable
                      );

                      return route.auth ? (
                        <PrivateRoute
                          {...route}
                          key={route.key}
                          collapsed={collapsed}
                          toggleCollapsed={toggleCollapsed}
                          showHiddenTriggerKeyPress={showHiddenTriggerKeyPress}
                          component={LazyComponent} 
                          locale={locale}
                        />
                      ) : (
                        <Route {...route} key={route.key} component={LazyComponent} />
                      );
                    })}
                  </Switch>
                </Suspense>

in routes array, I pass the component directly in the prop route.component.

Now lazy loading has really improved loading speed for me, so this is a good thing. But whenever something changes, all components are remounting instead they should be re-rendered. Please mention any probable fixes for this.

EDIT I didn't mention an example previously on how these components are remounting. So here's an example, I have setup a key, shift+X in my app.js which I use to show/hide some hidden items in my components.

const shiftXKeyPress = useKeyPress('X');
const showHiddenTriggerKeyPress = shiftXKeyPress;
    function useKeyPress(targetKey) {
    const [keyPressed, setKeyPressed] = useState(false);
    let prevKey = '';

    function downHandler({ key }) {
    if (prevKey === targetKey) return;
    if (key === targetKey) {
      setKeyPressed(!keyPressed);
      prevKey = targetKey;
    }
  }

    useEffect(() => {
    window.addEventListener('keydown', downHandler);
    return () => {
      window.removeEventListener('keydown', downHandler);
    };
  }, [keyPressed]);

  return keyPressed;
}

I have passed showHiddenTriggerKeyPress as a prop to all components, see in the above code for App.js. So whenever I press shift+x it should just re-render and display related contents, instead the whole component is reloading.


Solution

  • This will be a long answer. So I made some of my custom configurations to fix this, also took help from the answer by @ShehanLakshita.

    Firstly I wrapped PrivateRoute and Route with memo to prevent unnecessary re renders,

    const MemoizedRoute = React.memo(({ isPrivate, component: Component, ...rest }) => {
    if (isPrivate) {
      return (
        <PrivateRoute
          {...rest}
          component={Component} 
        />
      );
    }
    return (
      <Route
        {...rest}
        render={routeProps => <Component {...routeProps} {...rest} />}
      />
    );
    });
    

    I modified my loadable component to cache the LazyComponent correctly as suggested by @Shehan,

    const cachedComponentsMap = new Map();
    
    const useCachedLazy = (importFunc, namedExport) => {
      try {
        if (!cachedComponentsMap.has(importFunc)) {
          const LazyComponent = lazy(() =>
            importFunc().then(module => ({
              default: namedExport ? module[namedExport] : module.default,
            }))
          );
          cachedComponentsMap.set(importFunc, LazyComponent);
        }
        return cachedComponentsMap.get(importFunc);
      } catch (error) {
        console.error('Error loading component:', error);
        throw error; 
      }
    };
    
    
    const loadable = (importFunc, { namedExport = null } = {}) => {
      const cachedKey = `${importFunc}-${namedExport}`;
    
      if (!cachedComponentsMap.has(cachedKey)) {
        const MemoizedLazyComponent = React.memo(props => {
          const LazyComponent = useCachedLazy(importFunc, namedExport);
          return <LazyComponent {...props} />;
        });
    
        cachedComponentsMap.set(cachedKey, MemoizedLazyComponent);
      }
    
      return cachedComponentsMap.get(cachedKey);
    };
    

    Now the components do not unmount and remount unnecessary and I also got lazy working and the performance is improved.