import { RefObject, useState } from "react";
import { useIsomorphicInsertionEffect } from "./useIsomorphicLayoutEffect";
/**
* `useStyleSheet` is a hook that injects CSS code into the document's
*
* 1. Injects styles only once when called in multiple components
* 2. Gargabe collects the style when the last component is unmounted
*/
type StyleElementMap = WeakMap<
Document,
{
element: HTMLStyleElement;
// holds the number of active components that are using the same style sheet
// so we can garbage collect the style when the last component is unmounted
refCount: number;
}
>;
const styleElementMap: StyleElementMap = new Map();
/**
* Injects CSS code into the document's
*/
export const useStyleSheet = (styles: string | null, nodeRef: RefObject): void => {
const [css] = useState(() => styles); // css is constant and can't be changed between renders
const [parentDocument] = useState(() =>
/* note: `document` can be `undefined` in some environments like SSR */
nodeRef.current ? nodeRef.current.ownerDocument : globalThis.document
);
useIsomorphicInsertionEffect(() => {
if (!css || typeof parentDocument === "undefined") return;
if (!styleElementMap.has(parentDocument)) {
const styleElement = parentDocument.createElement("style");
styleElement.innerHTML = css;
styleElement.setAttribute("data-spoiled", "");
styleElementMap.set(parentDocument, { element: styleElement, refCount: 0 });
parentDocument.head.appendChild(styleElement);
}
styleElementMap.get(parentDocument)!.refCount++;
}, []);
useIsomorphicInsertionEffect(() => {
return () => {
if (!css || typeof parentDocument === "undefined") return;
const entry = styleElementMap.get(parentDocument);
// remove the CSS when the last component is unmounted
if (entry && --entry.refCount <= 0) {
styleElementMap.delete(parentDocument);
parentDocument.head.removeChild(entry.element);
}
};
}, []);
};