Skip to content

Instantly share code, notes, and snippets.

@max-barry
Last active May 25, 2021 11:53
Show Gist options
  • Save max-barry/4b87ec37071e79ddae774436a49063c7 to your computer and use it in GitHub Desktop.
Save max-barry/4b87ec37071e79ddae774436a49063c7 to your computer and use it in GitHub Desktop.
Lazy load a module using a React hook, with Typescript safety
import React, { useContext, useEffect, useRef, useState } from "react";
// Types
type Import<M> = () => Promise<M>;
/**
* Using a single hook without context.
* This is shorter but relies on the browser/webpack cache.
*
* @example
* const lodash = useLazyModule(
* () => import("lodash")
* );
*/
export function useLazyModule<M>(importer: Import<M>): M | undefined {
/** Get currently loaded Modules */
const [module, setModule] = useState<M | undefined>(undefined);
/** Use a ref to ensure we don't load twice */
const hasLoadedModule = useRef(!!module);
/** onMount call the module loader */
useEffect(() => {
/** Do this only once */
if (hasLoadedModule.current) return;
hasLoadedModule.current = true;
/** Load the module */
importer().then(setModule).catch(console.error);
}, [importer]);
return module as any;
}
/**
* Using context (aka the long way)
* Might be overkill because the browser/Webpack will cache this
*
* @example
* <LazyModules><Application /></LazyModules>
*/
/**
interface Modules {
[key: string]: unknown | {};
}
interface LazyModuleContextInterface {
modules: Modules;
loadModule<M>(name: string, importer: Import<M>): void;
}
const defaultLazyModuleContext: LazyModuleContextInterface = {
modules: {},
loadModule: () => {}
};
const LazyModuleContext = React.createContext(defaultLazyModuleContext);
const LazyModule: React.FC = ({ children }) => {
const [modules, setModules] = useState(defaultLazyModuleContext["modules"]);
function loadModule<M>(name: string, importer: Import<M>) {
importer()
.then((resolved: any) => {
setModules(current => ({ ...current, [name]: resolved }));
})
.catch((error: Error) => {
console.error(error);
setModules(current => ({ ...current, [name]: null }));
});
}
return (
<LazyModuleContext.Provider value={{ modules, loadModule }}>
{children}
</LazyModuleContext.Provider>
);
};
export default LazyModule;
export function useLazyModule<M>(importer: Import<M>): M | undefined {
// Get currently loaded Modules
const { modules, loadModule } = useContext(LazyModuleContext);
// Serialize our importing function
const reference = importer.toString().replace(/\/\*[\s\S]*?\*\/|\/\/.*\/g, "");
// Get the loaded module (eventually)
const loaded = modules[reference];
// Use a ref to ensure we don't load twice
const hasLoadedModule = useRef(!!loaded);
// onMount call the module loader
useEffect(() => {
// Do this only once
if (hasLoadedModule.current) return;
hasLoadedModule.current = true;
// Load the module
loadModule(reference, importer);
}, [importer, loadModule, reference]);
return (modules[reference] as any) || undefined;
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment