react-lazy-svg
Version:
react-lazy-svg is a simple way to use SVGs with the performance benefits of a sprite-sheet and svg css styling possibilities. Without bloating the bundle. It automatically creates a sprite-sheet for all used SVGs on the client but also provides the option
150 lines (145 loc) • 5.62 kB
JavaScript
import React, { useRef, createContext, useCallback, useContext, useMemo, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
const defaultInternalSpriteSheetId = '__SVG_SPRITE_SHEET__';
const isSSR = typeof document === 'undefined';
const hidden = {
height: 0,
width: 0,
position: 'absolute',
visibility: 'hidden',
};
function SpriteSheet({ icons, spriteSheetId = defaultInternalSpriteSheetId, embeddedSSR, }) {
const spriteSheetContainer = useRef(!isSSR && !embeddedSSR ? document.getElementById(spriteSheetId) : null);
const renderedIcons = icons.map(({ id, svgString, attributes: { width, height, ['xmlns:xlink']: xmlnsXlink, ...attributes }, }) => {
return (React.createElement("symbol", { key: id, id: id, xmlnsXlink: xmlnsXlink, ...attributes, dangerouslySetInnerHTML: svgString }));
});
if (spriteSheetContainer.current) {
return createPortal(renderedIcons, spriteSheetContainer.current);
}
return (React.createElement("svg", { id: spriteSheetId, style: hidden }, renderedIcons));
}
const globalIconsCache = new Map();
const noop = () => undefined;
const spriteContext = createContext({ registerSVG: noop });
const mapAttributes = (rawAttributes) => {
let match = null;
const attributes = {};
const attributesRegex = /(.*?)=["'](.*?)["']/gim;
while ((match = attributesRegex.exec(rawAttributes))) {
const [, name, value] = match;
if (name && value) {
attributes[name.trim()] = value;
}
}
return attributes;
};
let localIconsList = [];
const registeredListeners = new Set();
const addListener = (listener) => {
registeredListeners.add(listener);
listener(localIconsList);
};
const addIcon = (icon) => {
localIconsList = [...localIconsList, icon];
for (const listener of registeredListeners) {
listener(localIconsList);
}
};
const parseSVG = (url, svgString) => {
if (svgString) {
const svgRegex = /<svg([\s\S]*?)>([\s\S]*?)<\/svg>/gim;
const matches = svgRegex.exec(svgString);
if (matches) {
const [, attributesString, htmlString] = matches;
if (!attributesString || !htmlString) {
return;
}
const attributes = mapAttributes(attributesString);
const svgString = {
__html: htmlString.trim(),
};
const id = url;
return { id, svgString, attributes };
}
}
return undefined;
};
const registerIconInCache = (url, svgString, knownIcons) => {
const iconData = parseSVG(url, svgString);
if (iconData) {
if (!isSSR) {
addIcon(iconData);
}
}
else if (knownIcons.has(url)) {
knownIcons.delete(url);
}
return iconData;
};
const useIcons = () => {
const [icons, setIcons] = useState(localIconsList);
useEffect(() => {
addListener(setIcons);
return () => {
registeredListeners.delete(setIcons);
};
}, []);
return icons;
};
const SpriteContextProvider = ({ children, loadSVG, knownIcons = globalIconsCache, embeddedSSR = false, }) => {
const icons = useIcons();
const registerSVG = useCallback((url) => {
if (knownIcons.has(url)) {
return;
}
const iconPromise = loadSVG(url).then((svgString) => registerIconInCache(url, svgString, knownIcons));
knownIcons.set(url, iconPromise);
}, [knownIcons, loadSVG]);
const { registerSVG: wrappingContextRegisterSVG } = useContext(spriteContext);
const contextValue = useMemo(() => ({
registerSVG: wrappingContextRegisterSVG === noop
? registerSVG
: wrappingContextRegisterSVG,
}), [registerSVG, wrappingContextRegisterSVG]);
return (React.createElement(spriteContext.Provider, { value: contextValue },
children,
(!isSSR || embeddedSSR) && (React.createElement(SpriteSheet, { embeddedSSR: embeddedSSR, icons: icons }))));
};
const Icon = ({ url, ...props }) => {
const { registerSVG } = useContext(spriteContext);
if (isSSR) {
registerSVG(url);
}
else {
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
registerSVG(url);
}, [registerSVG, url]);
}
return (React.createElement("svg", { ...props },
React.createElement("use", { xlinkHref: `#${url}` })));
};
const mapNodeAttributes = (rawAttributes) => Array.from(rawAttributes).reduce((attributes, current) => {
attributes[current.name] = current.value;
return attributes;
}, {});
const initOnClient = (knownIcons = globalIconsCache, spriteSheetId = defaultInternalSpriteSheetId) => {
knownIcons.clear();
const spriteSheet = document.getElementById(spriteSheetId);
if (spriteSheet) {
const sprites = Array.from(spriteSheet.querySelectorAll('symbol'));
for (const node of sprites) {
const innerHTML = node.innerHTML;
const { id, attributes: rawAttributes } = node;
const attributes = mapNodeAttributes(rawAttributes);
const iconData = {
id,
attributes,
svgString: { __html: innerHTML.trim() },
};
addIcon(iconData);
knownIcons.set(id, new Promise((resolve) => resolve(iconData)));
}
}
};
export { Icon, SpriteContextProvider, initOnClient };