Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -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.
32 changes: 27 additions & 5 deletions client/src/components/base/IconifyIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -33,18 +33,40 @@ const iconSets: Record<string, IconifyJSON> = {
"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<string, IconDataCacheValue>();

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 = ({
Expand Down