@equinor/fusion-react-styles
Version:
style lib inspired by @material-ui/styles
101 lines (100 loc) • 5.42 kB
JavaScript
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;