The dep-only fix wasn't enough: better-auth's useSession (nanostores) can re-emit on focus / store refresh, transitioning session.user.id through undefined and back. Each transition re-fires the effect, which then calls setTheme(session.user.theme) with the stale cached value and reverts the user's selection. Track "have we synced this tab session" with a ref. After the first successful sync, no subsequent effect fire can revert. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
47 lines
1.7 KiB
TypeScript
47 lines
1.7 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* ThemeSessionBridge (THEME-03 / D-16, D-17).
|
|
*
|
|
* On session load and after sign-in, compares session.user.theme to the
|
|
* next-themes useTheme() value and calls setTheme(session.user.theme) if
|
|
* different. Server is canonical; this is the bridge that enforces it.
|
|
*
|
|
* Renders nothing.
|
|
*/
|
|
|
|
import { useEffect, useRef } from 'react';
|
|
import { useTheme } from 'next-themes';
|
|
import { useSession } from '@/lib/auth-client';
|
|
|
|
type SessionUserWithTheme = { theme?: 'light' | 'dark' | 'system' | string };
|
|
|
|
export function ThemeSessionBridge() {
|
|
const { data: session } = useSession();
|
|
const { theme, setTheme } = useTheme();
|
|
// Sync server → client EXACTLY ONCE per tab session. better-auth's
|
|
// useSession (nanostores) can re-emit on focus / store refresh, which would
|
|
// otherwise call setTheme with the stale cached session.user.theme and
|
|
// revert the user's just-made selection. The PUT to /api/me/theme does NOT
|
|
// refresh the cached session, so any post-selection re-fire would clobber.
|
|
const hasSynced = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (hasSynced.current) return;
|
|
if (!session?.user) return;
|
|
const serverTheme = (session.user as SessionUserWithTheme).theme;
|
|
// Only enforce explicit choices. 'system' is the column default and may
|
|
// mean "no opinion yet" — don't clobber a user's existing client preference
|
|
// (especially relevant on first load after the migration that backfilled
|
|
// every existing user to 'system').
|
|
if (serverTheme === 'light' || serverTheme === 'dark') {
|
|
if (serverTheme !== theme) {
|
|
setTheme(serverTheme);
|
|
}
|
|
}
|
|
hasSynced.current = true;
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [session?.user?.id, setTheme]);
|
|
|
|
return null;
|
|
}
|