fix: ssr rendering

This commit is contained in:
2026-04-28 10:26:33 +02:00
parent 92ef6fa5a9
commit 4f20ff33ae
2 changed files with 57 additions and 38 deletions
+1
View File
@@ -19,6 +19,7 @@ export function LocaleSwitcher() {
return ( return (
<div className="flex items-center gap-2 w-full py-2 hover:opacity-80"> <div className="flex items-center gap-2 w-full py-2 hover:opacity-80">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
src={`/flags/${currentLocale}.svg`} src={`/flags/${currentLocale}.svg`}
alt={currentLocale} alt={currentLocale}
+56 -38
View File
@@ -1,16 +1,16 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useCallback, useSyncExternalStore } from 'react';
type Theme = 'light' | 'dark'; type Theme = 'light' | 'dark';
function getCookie(name: string) { // ── Helpers ──────────────────────────────────────────────────────────────────
if (typeof document === 'undefined') return null;
function getCookie(name: string): string | null {
if (typeof document === 'undefined') return null;
const value = `; ${document.cookie}`; const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`); const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() ?? null;
if (parts.length === 2) return parts.pop()?.split(';').shift();
return null; return null;
} }
@@ -18,44 +18,62 @@ function setCookie(value: Theme) {
document.cookie = `theme=${value}; path=/; max-age=31536000`; document.cookie = `theme=${value}; path=/; max-age=31536000`;
} }
function readThemeFromDOM(): Theme {
const cookieTheme = getCookie('theme') as Theme | null;
if (cookieTheme === 'light' || cookieTheme === 'dark') return cookieTheme;
if (document.documentElement.classList.contains('dark')) return 'dark';
return 'light';
}
function applyThemeToDOM(t: Theme) {
document.documentElement.classList.toggle('dark', t === 'dark');
document.documentElement.classList.toggle('light-mode', t === 'light');
}
// ── External store ────────────────────────────────────────────────────────────
// useSyncExternalStore requires a stable subscribe function and a getSnapshot.
// We use a simple Set of listeners to notify React when the theme changes.
const listeners = new Set<() => void>();
let clientTheme: Theme | null = null; // lazily initialised on first client read
function getSnapshot(): Theme {
if (clientTheme === null) {
clientTheme = readThemeFromDOM();
}
return clientTheme;
}
// The server always returns the default theme so SSR HTML is deterministic.
// The client will then reconcile with the real cookie/DOM value via getSnapshot.
function getServerSnapshot(): Theme {
return 'dark';
}
function subscribe(callback: () => void): () => void {
listeners.add(callback);
return () => listeners.delete(callback);
}
function setGlobalTheme(next: Theme) {
clientTheme = next;
applyThemeToDOM(next);
setCookie(next);
listeners.forEach((cb) => cb());
}
// ── Hook ──────────────────────────────────────────────────────────────────────
export function useTheme() { export function useTheme() {
const [theme, setTheme] = useState<Theme>('dark'); const theme = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
useEffect(() => { const toggleTheme = useCallback(() => {
const cookieTheme = getCookie('theme') as Theme | null; setGlobalTheme(theme === 'dark' ? 'light' : 'dark');
const rootTheme = document.documentElement.classList.contains('dark') }, [theme]);
? 'dark'
: 'light';
const initial = cookieTheme || rootTheme || 'dark';
setTheme(initial);
document.documentElement.classList.toggle('dark', initial === 'dark');
document.documentElement.classList.toggle(
'light-mode',
initial === 'light',
);
}, []);
function applyTheme(t: Theme) {
document.documentElement.classList.toggle('dark', t === 'dark');
document.documentElement.classList.toggle('light-mode', t === 'light');
setCookie(t);
}
function toggleTheme() {
setTheme((prev) => {
const next = prev === 'dark' ? 'light' : 'dark';
applyTheme(next);
return next;
});
}
return { return {
theme, theme,
dark: theme === 'dark', dark: theme === 'dark',
toggleTheme, toggleTheme,
}; };
} }