UNPKG

@equinor/fusion-react-styles

Version:
101 lines (100 loc) 5.42 kB
import { useMemo, useContext, useEffect, useRef } from 'react'; import { theme as defaultTheme } from './theme'; import { ThemeContext, StylesContext } from './utils/contexts'; import { defaultSheetManager } from './utils/sheet-manager'; /** * Unique identifier for this module/runtime scope * Generated once when the module loads to ensure isolation between dynamically loaded apps * All makeStyles instances without a custom name will share this scope ID and reuse the same stylesheet * * @internal This is an internal implementation detail for scope isolation */ const scopeId = Math.random().toString(36).substring(2, 15); /** @internal Counter for generating unique instance names when name is not provided */ let instanceCounter = 0; /** * Generates a unique instance name for a makeStyles hook * * @internal This is an internal utility function for creating unique cache keys * @param name - Optional custom name for the instance * @returns A unique instance name combining scopeId and name/counter */ const generateInstanceName = (name) => { instanceCounter += 1; return `${scopeId}::${name ?? `style-${instanceCounter}`}`; }; // Implementation export function makeStyles(stylesOrCreator, options = {}) { const { name, defaultTheme: optionsDefaultTheme = defaultTheme } = options; if (!name) { if (process.env.NODE_ENV === 'development') { // Warn when name is missing to help developers avoid performance issues // Without a name, all instances creates a unique cache key, causing unnecessary re-renders console.warn('No name provided for makeStyles. This can cause serious performance issues!'); } } const instanceName = generateInstanceName(name); const useStyles = (props = {}) => { // Get theme from context or use default const theme = useContext(ThemeContext) || optionsDefaultTheme; // Get JSS instance and class name generator from styles context const { jss, generateClassName } = useContext(StylesContext); // Store sheet info for cleanup - track if this is the first render const sheetInfoRef = useRef(null); const isFirstRender = useRef(true); // Memoize class names to prevent unnecessary recalculations const classes = useMemo(() => { // Resolve styles: if it's a function, call it with theme; otherwise use directly // Theme casting is safe because makeStyles ensures type compatibility const styles = typeof stylesOrCreator === 'function' ? stylesOrCreator(theme) : stylesOrCreator; // Get or create stylesheet from the sheet manager (handles caching) const sheetResult = defaultSheetManager.getOrCreateSheet(styles, theme, instanceName, jss, generateClassName, props, isFirstRender.current); // On first render, store sheet info and increment refs if (isFirstRender.current) { sheetInfoRef.current = { cacheKey: sheetResult.cacheKey, instanceId: sheetResult.instanceId, isMounted: true, }; isFirstRender.current = false; } else { // On subsequent renders, only update dynamic sheet if props changed // Clean up previous dynamic sheet to prevent memory leaks // Each render creates a new dynamic sheet for the updated props if (sheetInfoRef.current?.instanceId) { defaultSheetManager.removeDynamicSheet(sheetInfoRef.current.cacheKey, sheetInfoRef.current.instanceId); } // Update instanceId for new dynamic sheet // instanceId is only set if dynamic styles exist (from sheetResult) if (sheetInfoRef.current) { sheetInfoRef.current.instanceId = sheetResult.instanceId; } } // Cast to Record<ClassKey, string> to preserve type information for createStyles // The ClassKey type is inferred from the styles object structure // This type assertion is safe because JSS generates classes matching the style keys return sheetResult.classes; }, [theme, props, generateClassName, jss]); // Cleanup on unmount - remove sheet reference // Empty dependency array ensures cleanup only runs on unmount // sheetInfoRef is stable and doesn't need to be in dependencies useEffect(() => { return () => { // Clean up stylesheet when component unmounts // Decrement ref count and detach sheets to prevent memory leaks if (sheetInfoRef.current?.isMounted) { defaultSheetManager.removeSheet(sheetInfoRef.current.cacheKey, sheetInfoRef.current.instanceId); sheetInfoRef.current.isMounted = false; } }; }, []); return classes; }; // Type assertion: Return type depends on whether Props is required or optional // This conditional type allows TypeScript to infer the correct hook signature // When Props is empty (never), props become optional; otherwise they're required // Return type preserves ClassKey for type-safe access via createStyles return useStyles; } export default makeStyles;