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
6 changes: 3 additions & 3 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ThemeProvider } from "next-themes";
import { Analytics } from "@vercel/analytics/react";
import Script from "next/script";
import "./globals.css";
Expand All @@ -10,6 +9,7 @@ import { baseMetaData } from "./metadata";
import { defaultFont } from "../lib/font-config";
import { BotIdClient } from "botid/client";
import { env } from "@/env";
import CustomThemeProvider from "@/components/theme-provider";

export const metadata = baseMetaData;

Expand All @@ -35,7 +35,7 @@ export default function RootLayout({
<BotIdClient protect={protectedRoutes} />
</head>
<body className={`${defaultFont.className} font-sans antialiased`}>
<ThemeProvider attribute="class" defaultTheme="dark">
<CustomThemeProvider attribute="class" defaultTheme="dark">
<TooltipProvider>
<StorageProvider>
<ScenesMigrator>{children}</ScenesMigrator>
Expand All @@ -55,7 +55,7 @@ export default function RootLayout({
data-track-sessions={false}
/>
</TooltipProvider>
</ThemeProvider>
</CustomThemeProvider>
</body>
</html>
);
Expand Down
21 changes: 21 additions & 0 deletions apps/web/src/components/theme-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client"

import { ThemeProvider, ThemeProviderProps } from "next-themes";
import { useEffect, useState } from "react"

interface CustomThemeProviderProps extends ThemeProviderProps {
children: React.ReactNode;
}

export default function CustomThemeProvider({ children, ...props }: CustomThemeProviderProps) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);

if (!mounted) {
return <>{children}</>;
}
Comment on lines +10 to +18
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Missing ThemeProvider context causes runtime errors for child components.

Returning <>{children}</> without the ThemeProvider wrapper when !mounted (lines 16-18) breaks the theme context. Components like Toaster (which calls useTheme() as shown in the relevant snippet from sonner.tsx) will throw an error during the initial render: "useTheme must be used within a ThemeProvider".

The fix for hydration errors should keep the ThemeProvider wrapped at all times. The suppressHydrationWarning attribute on the <html> tag (already present in layout.tsx line 33) handles theme-related hydration mismatches.

Apply this diff to fix the context issue:

 export default function CustomThemeProvider({ children, ...props }: CustomThemeProviderProps) {
   const [mounted, setMounted] = useState(false);
   useEffect(() => {
     setMounted(true);
   }, []);

-  if (!mounted) {
-    return <>{children}</>;
-  }
-
-  return <ThemeProvider {...props}>{children}</ThemeProvider >
+  return (
+    <ThemeProvider {...props}>
+      {mounted ? children : null}
+    </ThemeProvider>
+  );
 }

Alternatively, if you want to avoid the layout shift from returning null, simply always render children within ThemeProvider and rely on suppressHydrationWarning:

 export default function CustomThemeProvider({ children, ...props }: CustomThemeProviderProps) {
-  const [mounted, setMounted] = useState(false);
-  useEffect(() => {
-    setMounted(true);
-  }, []);
-
-  if (!mounted) {
-    return <>{children}</>;
-  }
-
-  return <ThemeProvider {...props}>{children}</ThemeProvider >
+  return <ThemeProvider {...props}>{children}</ThemeProvider>;
 }

The second approach is simpler and leverages Next.js's built-in hydration handling via suppressHydrationWarning.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/web/src/components/theme-provider.tsx around lines 10 to 18, the
component currently returns <>children</> when !mounted which removes the
ThemeProvider and causes runtime errors (e.g., "useTheme must be used within a
ThemeProvider"); remove the early return and always render the ThemeProvider
wrapper around children (i.e., render ThemeProvider regardless of mounted state)
to preserve theme context during initial render and rely on the existing
suppressHydrationWarning in layout.tsx to handle hydration mismatches.


return <ThemeProvider {...props}>{children}</ThemeProvider >
}
6 changes: 5 additions & 1 deletion apps/web/src/components/theme-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ export function ThemeToggle({ className }: ThemeToggleProps) {
className="h-7"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
<Sun className="!size-[1.1rem]" />
{theme === "dark" ? (
<Sun className="!size-[1.1rem]" />
) : (<Moon className="!size-[1.1rem]" />)}
Comment on lines +21 to +23
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Potential hydration mismatch with undefined theme.

The theme value from useTheme() may be undefined during SSR or the first client render (before the CustomThemeProvider mounts), causing the conditional on line 21 to render the Moon icon. Once theme resolves on the client, it might switch to Sun if the theme is "dark", creating a hydration mismatch.

Consider guarding against undefined theme or deferring the render until theme is available.

Apply this diff to add a guard:

-      {theme === "dark" ? (
+      {theme === "dark" ? (
         <Sun className="!size-[1.1rem]" />
-      ) : (<Moon className="!size-[1.1rem]" />)}
+      ) : theme === "light" ? (
+        <Moon className="!size-[1.1rem]" />
+      ) : (
+        <Sun className="!size-[1.1rem]" />
+      )}

Alternatively, consider not rendering any icon until theme is defined:

-      {theme === "dark" ? (
-        <Sun className="!size-[1.1rem]" />
-      ) : (<Moon className="!size-[1.1rem]" />)}
+      {theme === "dark" ? (
+        <Sun className="!size-[1.1rem]" />
+      ) : theme === "light" ? (
+        <Moon className="!size-[1.1rem]" />
+      ) : null}

Run the following script to check if the CustomThemeProvider properly prevents hydration mismatches:


🏁 Script executed:

#!/bin/bash
# Description: Check CustomThemeProvider implementation and ThemeToggle usage

# Check CustomThemeProvider implementation for mounted state
echo "=== CustomThemeProvider implementation ==="
rg -A 20 'export.*CustomThemeProvider' --type tsx

# Check if ThemeToggle guards against undefined theme
echo -e "\n=== ThemeToggle usage of theme ==="
rg -B 2 -A 2 'theme ===' apps/web/src/components/theme-toggle.tsx

Length of output: 448


🏁 Script executed:

#!/bin/bash
# Better search for CustomThemeProvider without problematic type flag
echo "=== CustomThemeProvider implementation ==="
rg -A 20 'export.*CustomThemeProvider' 

# Also check the full theme-toggle.tsx file to see all code
echo -e "\n=== Full theme-toggle.tsx file ==="
cat apps/web/src/components/theme-toggle.tsx

# Check if there's a hooks file or provider file
echo -e "\n=== Looking for theme-related files ==="
fd -e tsx -e ts | grep -i theme | head -20

Length of output: 1736


Address potential hydration mismatch in conditional theme rendering.

The component renders different icons based on the theme value without guarding against undefined. During SSR and before the CustomThemeProvider mounts on the client, useTheme() returns theme: undefined, causing the conditional to render the Moon icon. Once the provider mounts and theme resolves (e.g., to "dark"), the component attempts to render the Sun icon instead, creating a hydration mismatch.

Add a guard to handle the undefined state:

-      {theme === "dark" ? (
+      {theme === "dark" ? (
         <Sun className="!size-[1.1rem]" />
-      ) : (<Moon className="!size-[1.1rem]" />)}
+      ) : theme === "light" ? (
+        <Moon className="!size-[1.1rem]" />
+      ) : null}

Or ensure the component doesn't render until theme is available by checking it within a useEffect or wrapping with a loading state.

🤖 Prompt for AI Agents
In apps/web/src/components/theme-toggle.tsx around lines 21 to 23, the
conditional renders icons directly from theme which can be undefined during SSR
causing hydration mismatches; update the component to guard against undefined by
checking theme before rendering (e.g., return null or a placeholder while theme
=== undefined) or use a mounted flag (set true in useEffect) and only render the
Sun/Moon once theme is defined, ensuring the same markup is produced on server
and client to avoid hydration errors.

<span className="sr-only">{theme === "dark" ? "Light" : "Dark"}</span>
</Button>
);
}


Loading