fix(hero): prevent SSR hydration mismatch for subtitle

Add isSubtitleLoaded flag to hide subtitle until client-side random
selection completes, preventing flash of wrong subtitle during hydration.

Changes:
- Add isSubtitleLoaded state to HomeHeroContext
- Set flag to true after subtitle is selected on client
- Add opacity transition to subtitle in HeroAbacus
- Matches loading pattern used for abacus value

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-10-20 14:30:00 -05:00
parent 48647e4fb5
commit 1bfde8fb25
2 changed files with 16 additions and 2 deletions

View File

@@ -6,7 +6,14 @@ import { css } from '../../styled-system/css'
import { useHomeHero } from '../contexts/HomeHeroContext'
export function HeroAbacus() {
const { subtitle, abacusValue, setAbacusValue, setIsHeroVisible, isAbacusLoaded } = useHomeHero()
const {
subtitle,
abacusValue,
setAbacusValue,
setIsHeroVisible,
isAbacusLoaded,
isSubtitleLoaded,
} = useHomeHero()
const appConfig = useAbacusConfig()
const heroRef = useRef<HTMLDivElement>(null)
@@ -101,6 +108,8 @@ export function HeroAbacus() {
color: 'purple.300',
fontStyle: 'italic',
marginBottom: '8',
opacity: isSubtitleLoaded ? 1 : 0,
transition: 'opacity 0.5s ease-in-out',
})}
>
{subtitle.text}

View File

@@ -12,6 +12,7 @@ interface HomeHeroContextValue {
isHeroVisible: boolean
setIsHeroVisible: (visible: boolean) => void
isAbacusLoaded: boolean
isSubtitleLoaded: boolean
}
const HomeHeroContext = createContext<HomeHeroContextValue | null>(null)
@@ -21,6 +22,7 @@ export { HomeHeroContext }
export function HomeHeroProvider({ children }: { children: React.ReactNode }) {
// Use first subtitle for SSR, then select random one on client mount
const [subtitle, setSubtitle] = useState<Subtitle>(subtitles[0])
const [isSubtitleLoaded, setIsSubtitleLoaded] = useState(false)
// Select random subtitle only on client side, persist per-session
useEffect(() => {
@@ -32,6 +34,7 @@ export function HomeHeroProvider({ children }: { children: React.ReactNode }) {
const index = parseInt(storedIndex, 10)
if (!Number.isNaN(index) && index >= 0 && index < subtitles.length) {
setSubtitle(subtitles[index])
setIsSubtitleLoaded(true)
return
}
}
@@ -40,6 +43,7 @@ export function HomeHeroProvider({ children }: { children: React.ReactNode }) {
const randomIndex = Math.floor(Math.random() * subtitles.length)
sessionStorage.setItem('heroSubtitleIndex', randomIndex.toString())
setSubtitle(subtitles[randomIndex])
setIsSubtitleLoaded(true)
}, [])
// Shared abacus value - always start at 0 for SSR/hydration consistency
@@ -106,8 +110,9 @@ export function HomeHeroProvider({ children }: { children: React.ReactNode }) {
isHeroVisible,
setIsHeroVisible,
isAbacusLoaded,
isSubtitleLoaded,
}),
[subtitle, abacusValue, isHeroVisible, isAbacusLoaded]
[subtitle, abacusValue, isHeroVisible, isAbacusLoaded, isSubtitleLoaded]
)
return <HomeHeroContext.Provider value={value}>{children}</HomeHeroContext.Provider>