Skip to content
Merged
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
32 changes: 2 additions & 30 deletions src/components/ui/theme-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,41 +32,13 @@ export function ThemeSwitcher({
const t = useTranslations("common");
const [mounted, setMounted] = useState(false);

// Wrap useTheme in try-catch for localStorage error handling
let themeHook: ReturnType<typeof useTheme> | null = null;
try {
themeHook = useTheme();
} catch (error) {
console.error("Failed to initialize theme:", error);
}
// Always call useTheme unconditionally (Rules of Hooks requirement)
const { theme, setTheme } = useTheme();
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High: Removed error handling could cause component crashes

Why this is a problem: While the fix correctly addresses the React Hooks ESLint violation, removing the try-catch wrapper around useTheme() eliminates error handling for localStorage-related failures. The next-themes library can throw errors when localStorage is unavailable (e.g., in Safari private mode, when storage quota is exceeded, or when localStorage is disabled by browser policies). This will cause the entire component to crash instead of gracefully degrading.

Suggested fix:

export function ThemeSwitcher({
  className,
  size = "sm",
  showLabel = false,
}: ThemeSwitcherProps) {
  const t = useTranslations("common");
  const [mounted, setMounted] = useState(false);
  const [themeError, setThemeError] = useState(false);

  // Always call useTheme unconditionally (Rules of Hooks requirement)
  const { theme, setTheme } = useTheme();

  useEffect(() => {
    setMounted(true);
    // Test localStorage availability
    try {
      localStorage.setItem('theme-test', 'test');
      localStorage.removeItem('theme-test');
    } catch (error) {
      console.error('Failed to access localStorage:', error);
      setThemeError(true);
    }
  }, []);

  // Handle theme changes with error handling
  const handleThemeChange = (value: string) => {
    if (themeError) return;
    try {
      setTheme(value as ThemeValue);
    } catch (error) {
      console.error("Failed to change theme:", error);
      setThemeError(true);
    }
  };

  if (!mounted || themeError) {
    return (
      <Button
        aria-label={t("theme")}
        variant="ghost"
        size={size === "sm" ? "icon" : "default"}
        className={cn(
          "relative rounded-full border border-border/60 bg-card/60 text-muted-foreground",
          size === "sm" && "size-9",
          themeError && "cursor-not-allowed opacity-50",
          className
        )}
        disabled
        title={themeError ? "Theme system unavailable" : undefined}
      >
        <Sun className="size-4" style={{ opacity: themeError ? 0.5 : 0.6 }} />
        {showLabel && <span className="ml-2 text-sm">{t("theme")}</span>}
      </Button>
    );
  }

  // ... rest of component
}


useEffect(() => {
setMounted(true);
}, []);

// Graceful degradation if theme system unavailable
if (!themeHook) {
return (
<Button
aria-label={t("theme")}
variant="ghost"
size={size === "sm" ? "icon" : "default"}
className={cn(
"relative rounded-full border border-border/60 bg-card/60 text-muted-foreground cursor-not-allowed",
size === "sm" && "size-9",
className
)}
disabled
title="Theme system unavailable"
>
<Sun className="size-4 opacity-50" />
{showLabel && <span className="ml-2 text-sm opacity-50">{t("theme")}</span>}
</Button>
);
}

const { theme, setTheme } = themeHook;

// Simplified theme options with better type inference
const options = [
{ value: "light" as ThemeValue, icon: Sun },
Expand Down
Loading