UNPKG

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
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 };