diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..c23fa77 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2025-02-18 - IconifyIcon Component Next.js Cache Type Requirements +**Learning:** The custom `IconifyIcon` component (`client/src/components/base/IconifyIcon.tsx`) requires an O(N) array iteration to resolve icons from multiple loaded JSON icon sets. Caching these lookups is essential, especially caching negative results (missing icons). However, using `undefined` for a missing result cache breaks Next.js strict build validations since `@iconify/react`'s `IconProps["icon"]` implicitly expects an extended icon shape (`IconifyIcon & { [key: string]: any }`) or primitive types. +**Action:** When implementing a custom cache to memoize resolved Iconify icons from JSON imports inside Next.js components, define an explicit `ExtendedIconifyIcon | null` type. Store `null` instead of `undefined` for negative hits to satisfy `IconifyIcon | null` constraint without triggering bundle/build failures from `@iconify/react` type exports. diff --git a/client/src/components/base/IconifyIcon.tsx b/client/src/components/base/IconifyIcon.tsx index b48f0dc..b4d99c8 100644 --- a/client/src/components/base/IconifyIcon.tsx +++ b/client/src/components/base/IconifyIcon.tsx @@ -9,7 +9,7 @@ import { icons as mdiIcons } from "@iconify-json/mdi"; import { icons as mdiLightIcons } from "@iconify-json/mdi-light"; import { icons as riIcons } from "@iconify-json/ri"; import { icons as twemojiIcons } from "@iconify-json/twemoji"; -import { Icon, IconifyJSON, IconProps } from "@iconify/react"; +import { Icon, IconifyIcon as IconifyIconType, IconifyJSON, IconProps } from "@iconify/react"; import { getIconData } from "@iconify/utils"; import { Box, BoxProps } from "@mui/material"; @@ -33,18 +33,40 @@ const iconSets: Record = { "mdi-light": mdiLightIcons, }; +// ExtendedIconifyIcon constraint is required here to satisfy TypeScript build errors +// when using the cache for negative lookups (null) +type ExtendedIconifyIcon = IconifyIconType & { [key: string]: any }; +type IconDataCacheValue = ExtendedIconifyIcon | null; + +// Cache to prevent O(N) lookup loops across all imported icon sets for every icon render +const iconCache = new Map(); + const iconData = (icon: string) => { + if (iconCache.has(icon)) { + return iconCache.get(icon); + } + const [prefix, name] = icon.includes(":") ? icon.split(":") : ["", icon]; if (prefix && iconSets[prefix]) { - const data = getIconData(iconSets[prefix], name); - if (data) return data; + const data = getIconData(iconSets[prefix], name) as ExtendedIconifyIcon | null; + if (data) { + iconCache.set(icon, data); + return data; + } } for (const [_, icons] of Object.entries(iconSets)) { - const data = getIconData(icons, name); - if (data) return data; + const data = getIconData(icons, name) as ExtendedIconifyIcon | null; + if (data) { + iconCache.set(icon, data); + return data; + } } + + // Cache missing icons as null to avoid repeated failing O(N) lookups + iconCache.set(icon, null); + return null; }; const IconifyIcon = ({