Last active
May 25, 2021 11:53
-
-
Save max-barry/4b87ec37071e79ddae774436a49063c7 to your computer and use it in GitHub Desktop.
Lazy load a module using a React hook, with Typescript safety
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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