UNPKG

@dr.pogodin/react-utils

Version:

Collection of generic ReactJS components and utils

218 lines (209 loc) 6.92 kB
/* global document */ import { Suspense, lazy, useInsertionEffect } from 'react'; import { Barrier } from '@dr.pogodin/js-utils'; import { getSsrContext } from "./globalState"; import { IS_CLIENT_SIDE, IS_SERVER_SIDE, getBuildInfo } from "./isomorphy"; import { jsx as _jsx } from "react/jsx-runtime"; function getClientChunkGroups() { if (!IS_CLIENT_SIDE) return undefined; return (async () => { const { default: getInj } = await import(/* webpackChunkName: "react-utils-client-side-code" */"../../client/getInj"); return getInj().CHUNK_GROUPS ?? {}; })(); } const refCounts = {}; function getPublicPath() { return getBuildInfo().publicPath; } /** * Client-side only! Ensures the specified CSS stylesheet is loaded into * the document; loads if it is missing; and does simple reference counting * to facilitate future clean-up. * @param name * @param loadedSheets * @param refCount * @return */ function bookStyleSheet(name, loadedSheets, refCount) { let res; const path = `${getPublicPath()}/${name}`; const fullPath = `${document.location.origin}${path}`; if (!loadedSheets.has(fullPath)) { let link = document.querySelector(`link[href="${path}"]`); if (!link) { link = document.createElement('link'); link.setAttribute('rel', 'stylesheet'); link.setAttribute('href', path); document.head.appendChild(link); } res = new Barrier(); link.addEventListener('load', () => { if (!res) throw Error('Internal error'); void res.resolve(); }); link.addEventListener('error', () => { if (!res) throw Error('Internal error'); void res.resolve(); }); } if (refCount) { const current = refCounts[path] ?? 0; refCounts[path] = 1 + current; } return res; } /** * Generates the set of URLs for currently loaded, linked stylesheets. * @return */ function getLoadedStyleSheets() { const res = new Set(); const { styleSheets } = document; for (const { href } of styleSheets) { if (href) res.add(href); } return res; } function assertChunkName(chunkName, chunkGroups) { if (chunkGroups[chunkName]) return; throw Error(`Unknown chunk name "${chunkName}"`); } /** * Client-side only! Ensures all CSS stylesheets required for the specified * code chunk are loaded into the document; loads the missing ones; and does * simple reference counting to facilitate future clean-up. * @param chunkName Chunk name. * @param refCount * @return Resolves once all pending stylesheets, necessary for * the chunk, are either loaded, or failed to load. */ export async function bookStyleSheets(chunkName, chunkGroups, refCount) { const promises = []; const assets = chunkGroups[chunkName]; if (!assets) return Promise.resolve(); const loadedSheets = getLoadedStyleSheets(); for (const asset of assets) { if (asset.endsWith('.css')) { const promise = bookStyleSheet(asset, loadedSheets, refCount); if (promise) promises.push(promise); } } return promises.length ? Promise.allSettled(promises).then() : Promise.resolve(); } /** * Client-side only! Frees from the document all CSS stylesheets that are * required by the specified chunk, and have reference counter equal to one * (for chunks with larger reference counter values, it just decrements * the reference counter). * @param {string} chunkName */ export function freeStyleSheets(chunkName, chunkGroups) { const assets = chunkGroups[chunkName]; if (!assets) return; for (const asset of assets) { if (asset.endsWith('.css')) { const path = `${getPublicPath()}/${asset}`; const pathRefCount = refCounts[path]; if (pathRefCount) { if (pathRefCount <= 1) { document.head.querySelector(`link[href="${path}"]`).remove(); delete refCounts[path]; } else refCounts[path] = pathRefCount - 1; } } } } // Holds the set of chunk names already used for splitComponent() calls. const usedChunkNames = new Set(); /** * Given an async component retrieval function `getComponent()` it creates * a special "code split" component, which uses <Suspense> to asynchronously * load on demand the code required by `getComponent()`. * @param options * @param options.chunkName * @param {function} options.getComponent * @param {React.Element} [options.placeholder] * @return {React.ElementType} */ export default function splitComponent({ chunkName, getComponent, placeholder }) { // The correct usage of splitComponent() assumes a single call per chunk. if (usedChunkNames.has(chunkName)) { throw Error(`Repeated splitComponent() call for the chunk "${chunkName}"`); } else usedChunkNames.add(chunkName); const LazyComponent = /*#__PURE__*/lazy(async () => { const clientChunkGroups = await getClientChunkGroups(); // On the client side we can check right away if the chunk name is known. if (IS_CLIENT_SIDE) { if (!clientChunkGroups) throw Error('Internal error'); assertChunkName(chunkName, clientChunkGroups); } const resolved = await getComponent(); const Component = 'default' in resolved ? resolved.default : resolved; // This pre-loads necessary stylesheets prior to the first mount of // the component (the lazy load function is executed by React one at // the frist mount). if (IS_CLIENT_SIDE) { if (!clientChunkGroups) throw Error('Internal error'); await bookStyleSheets(chunkName, clientChunkGroups, false); } const Wrapper = ({ children, ref, ...rest }) => { // On the server side we'll assert the chunk name here, // and also push it to the SSR chunks array. if (IS_SERVER_SIDE) { const { chunkGroups, chunks } = getSsrContext(); assertChunkName(chunkName, chunkGroups); if (!chunks.includes(chunkName)) chunks.push(chunkName); } // This takes care about stylesheets management every time an instance of // this component is mounted / unmounted. useInsertionEffect(() => { if (!clientChunkGroups) throw Error('Internal error'); void bookStyleSheets(chunkName, clientChunkGroups, true); return () => { freeStyleSheets(chunkName, clientChunkGroups); }; }, []); return /*#__PURE__*/_jsx(Component // eslint-disable-next-line react/jsx-props-no-spreading , { ...rest, ref: ref, children: children }); }; return { default: Wrapper }; }); const CodeSplit = ({ children, ...rest }) => /*#__PURE__*/_jsx(Suspense, { fallback: placeholder, children: /*#__PURE__*/_jsx(LazyComponent // eslint-disable-next-line react/jsx-props-no-spreading , { ...rest, children: children }) }); return CodeSplit; } //# sourceMappingURL=splitComponent.js.map