fix: resolve SSR/client hydration mismatch for themed navigation

Add hydration state tracking to GameThemeContext to prevent flash of unstyled content:
- Track isHydrated state in GameThemeContext
- Only apply themed backgrounds and game names after client hydration
- Prevents Next.js hydration mismatch where server renders default styles but client overwrites with themed styles
- Eliminates the brief flash where themed navigation appears then reverts to default

Navigation theming now applies consistently without visual flashing.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-28 12:15:49 -05:00
parent 00bfcbcdee
commit 301e65dfa6
2 changed files with 12 additions and 5 deletions

View File

@@ -18,11 +18,12 @@ export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
const isGamePage = pathname?.startsWith('/games')
const isArcadePage = pathname?.startsWith('/arcade')
const { isFullscreen, toggleFullscreen, exitFullscreen } = useFullscreen()
const { theme: gameTheme } = useGameTheme()
const { theme: gameTheme, isHydrated } = useGameTheme()
// Helper function to get themed background colors
const getThemedBackground = (opacity: number = 0.85) => {
if (gameTheme?.backgroundColor) {
// Only apply theming after hydration to prevent SSR/client mismatch
if (isHydrated && gameTheme?.backgroundColor) {
const color = gameTheme.backgroundColor
if (color.startsWith('#')) {
// Convert hex to rgba
@@ -103,7 +104,7 @@ export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
backgroundClip: 'text',
color: 'transparent'
})}>
🕹 {gameTheme?.gameName || (isArcadePage ? 'Arcade' : 'Game')}
🕹 {(isHydrated && gameTheme?.gameName) || (isArcadePage ? 'Arcade' : 'Game')}
</h1>
<div className={css({
px: '2',

View File

@@ -1,6 +1,6 @@
'use client'
import { createContext, useContext, useState, ReactNode } from 'react'
import { createContext, useContext, useState, useEffect, ReactNode } from 'react'
export interface GameTheme {
gameName: string
@@ -10,15 +10,21 @@ export interface GameTheme {
interface GameThemeContextType {
theme: GameTheme | null
setTheme: (theme: GameTheme | null) => void
isHydrated: boolean
}
const GameThemeContext = createContext<GameThemeContextType | undefined>(undefined)
export function GameThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<GameTheme | null>(null)
const [isHydrated, setIsHydrated] = useState(false)
useEffect(() => {
setIsHydrated(true)
}, [])
return (
<GameThemeContext.Provider value={{ theme, setTheme }}>
<GameThemeContext.Provider value={{ theme, setTheme, isHydrated }}>
{children}
</GameThemeContext.Provider>
)