From c6657694f7dde543aa6f7c50f19f4b8d398dfa9f Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Tue, 2 Sep 2025 23:58:35 -0700 Subject: [PATCH] Fix hydration mismatch causing avatar flash on page load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Read auth cookies server-side in layout.tsx - Pass initial auth data as props to AccountStateInitializer - Initialize Valtio state synchronously before first render - Fallback to client-side cookie reading only when no server data - Eliminates anonymous avatar flash during hydration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/[locale]/layout.tsx | 25 +++++++- components/AccountStateInitializer/index.tsx | 62 +++++++++++++++++++- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index a77534a1..44d79e0c 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -3,6 +3,7 @@ import localFont from 'next/font/local' import { NextIntlClientProvider } from 'next-intl' import { getMessages } from 'next-intl/server' import { Viewport as ToastViewport } from '@radix-ui/react-toast' +import { cookies } from 'next/headers' import { locales } from '../../i18n.config' import '../../styles/globals.scss' @@ -49,6 +50,28 @@ export default async function LocaleLayout({ // Load messages for the locale const messages = await getMessages() + // Parse auth cookies on server + const cookieStore = cookies() + const accountCookie = cookieStore.get('account') + const userCookie = cookieStore.get('user') + + let initialAuthData = null + if (accountCookie && userCookie) { + try { + const accountData = JSON.parse(accountCookie.value) + const userData = JSON.parse(userCookie.value) + + if (accountData && accountData.token) { + initialAuthData = { + account: accountData, + user: userData + } + } + } catch (error) { + console.error('Error parsing auth cookies on server:', error) + } + } + // Fetch version data on the server let version = null try { @@ -68,7 +91,7 @@ export default async function LocaleLayout({ - +
diff --git a/components/AccountStateInitializer/index.tsx b/components/AccountStateInitializer/index.tsx index c2520b06..f9ceed73 100644 --- a/components/AccountStateInitializer/index.tsx +++ b/components/AccountStateInitializer/index.tsx @@ -1,12 +1,68 @@ 'use client' -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import { getCookie } from 'cookies-next' import { accountState } from '~utils/accountState' import { setHeaders } from '~utils/userToken' -export default function AccountStateInitializer() { +interface InitialAuthData { + account: { + token: string + userId: string + username: string + role: number + } + user: { + avatar: { + picture: string + element: string + } + gender: number + language: string + theme: string + bahamut?: boolean + } +} + +interface AccountStateInitializerProps { + initialAuthData?: InitialAuthData | null +} + +export default function AccountStateInitializer({ initialAuthData }: AccountStateInitializerProps) { + const initialized = useRef(false) + + // Initialize synchronously on first render if we have server data + if (initialAuthData && !initialized.current) { + initialized.current = true + const { account: accountData, user: userData } = initialAuthData + + console.log(`Logged in as user "${accountData.username}"`) + + // Set headers for API calls + setHeaders() + + // Update account state + accountState.account.authorized = true + accountState.account.user = { + id: accountData.userId, + username: accountData.username, + role: accountData.role, + granblueId: '', + avatar: { + picture: userData.avatar.picture, + element: userData.avatar.element, + }, + gender: userData.gender, + language: userData.language, + theme: userData.theme, + bahamut: userData.bahamut || false, + } + } + useEffect(() => { + // Only run client-side cookie reading if no server data + if (initialized.current) return + const accountCookie = getCookie('account') const userCookie = getCookie('user') @@ -37,6 +93,8 @@ export default function AccountStateInitializer() { theme: userData.theme, bahamut: userData.bahamut || false, } + + initialized.current = true } } catch (error) { console.error('Error parsing account cookies:', error)