diff --git a/packages/styling/package.json b/packages/styling/package.json index 5d789a38..2d200356 100644 --- a/packages/styling/package.json +++ b/packages/styling/package.json @@ -48,7 +48,7 @@ "vitest": "^1.1.0" }, "peerDependencies": { - "react": "^16.8 || ^17 || ^18 || ^19.0.0-0", - "react-dom": "^16.8 || ^17 || ^18 || ^19.0.0-0" + "react": "^19.0.0-0", + "react-dom": "^19.0.0-0" } } diff --git a/packages/styling/src/Style.tsx b/packages/styling/src/Style.tsx index 00e4d6c4..c87d43e9 100644 --- a/packages/styling/src/Style.tsx +++ b/packages/styling/src/Style.tsx @@ -1,65 +1,25 @@ -import * as React from 'react'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import React from 'react'; -import { - getRegisteredStyles, - registerStyles, - unregisterStyles, - updateStyles, -} from './style-registry.js'; - -const { useCallback, useEffect, useMemo, useRef, useState } = React; +import { useStyles } from './useStyles.js'; type Props = { children: string; + precedence?: string; }; -const Style = ({ children }: Props) => { +const Style = ({ children, precedence = 'medium' }: Props) => { // Minify CSS styles by replacing consecutive whitespace (including \n) with ' ' - const styles = useMemo(() => children.replace(/\s+/gm, ' '), [children]); - const [ownerDocument, setOwnerDocument] = useState(null); - const isMountedRef = useRef(false); - - useEffect(() => { - isMountedRef.current = true; - unregisterStyles({ ownerDocument: 'global', styles }); - }, []); // eslint-disable-line react-hooks/exhaustive-deps - - useEffect( - () => () => { - if (!ownerDocument) return; - unregisterStyles({ ownerDocument, styles }); - }, - [ownerDocument], // eslint-disable-line react-hooks/exhaustive-deps + const styles = useStyles(children); + // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/canary.d.ts + // https://react.dev/reference/react-dom/components/style#props + return ( + // @ts-expect-error @types/react is missing new ); - - const previousStylesRef = useRef(''); - - useEffect(() => { - if (!ownerDocument) return; - - updateStyles({ - ownerDocument, - previousStyles: previousStylesRef.current, - styles, - }); - - previousStylesRef.current = styles; - }, [ownerDocument, styles]); - - const handleRef = useCallback((element: HTMLElement | null) => { - if (!element) return; - setOwnerDocument(element.ownerDocument); - }, []); - - if (ownerDocument) return null; - - // Avoid duplicate style rendering during SSR via style registry - if (!isMountedRef.current) { - if (getRegisteredStyles({ ownerDocument: 'global', styles })) return null; - registerStyles({ ownerDocument: 'global', styles }); - } - - return + + , + ); + + let stylesItemA = styleRegistry.get(mockStylesA); + expect(stylesItemA?.referenceCount).toBe(2); + expect(stylesItemA?.styles).toBe('.test-a { color: cyan; }'); + expect(styleRegistry.size).toBe(1); + + rerender(); + expect(stylesItemA?.referenceCount).toBe(1); + expect(stylesItemA).toBe(styleRegistry.get(mockStylesA)); + expect(styleRegistry.size).toBe(1); + + rerender(); + stylesItemA = styleRegistry.get(mockStylesA); + expect(stylesItemA).toBe(undefined); + let stylesItemB = styleRegistry.get(mockStylesB); + expect(stylesItemB?.referenceCount).toBe(1); + expect(styleRegistry.size).toBe(1); + + rerender( + + + + , + ); + stylesItemA = styleRegistry.get(mockStylesA); + expect(stylesItemA?.referenceCount).toBe(1); + expect(stylesItemA).toBe(styleRegistry.get(mockStylesA)); + stylesItemB = styleRegistry.get(mockStylesB); + expect(stylesItemB?.referenceCount).toBe(1); + expect(styleRegistry.size).toBe(2); + + rerender(
); + expect(styleRegistry.size).toBe(0); + }); + }); +}); diff --git a/packages/styling/src/useStyles.ts b/packages/styling/src/useStyles.ts new file mode 100644 index 00000000..3c0bb9cb --- /dev/null +++ b/packages/styling/src/useStyles.ts @@ -0,0 +1,58 @@ +import { useEffect, useState } from 'react'; + +import { minifyStyles } from './minifyStyles.js'; + +type StyleRegistry = Map; + +const styleRegistry: StyleRegistry = new Map(); + +export const getStyleRegistry = () => styleRegistry; + +export function useStyles(styles: string) { + const [minifiedStyles, setMinifiedStyles] = useState(() => { + if (!styles) return ''; + + let minified = ''; + const existingStylesItem = styleRegistry.get(styles); + if (existingStylesItem) { + existingStylesItem.referenceCount++; + minified = existingStylesItem.styles; + } else { + minified = minifyStyles(styles); + styleRegistry.set(styles, { referenceCount: 1, styles: minified }); + } + + return minified; + }); + + useEffect(() => { + if (!styles) return; + if (!styleRegistry.get(styles)) { + const minified = minifyStyles(styles); + styleRegistry.set(styles, { referenceCount: 1, styles: minified }); + setMinifiedStyles(minified); + } + + return () => { + const stylesItem = styleRegistry.get(styles); + if (stylesItem) { + stylesItem.referenceCount--; + if (!stylesItem.referenceCount) { + // TODO try scheduling this via setTimeout + // and add another referenceCount check + // to deal with instance where existing