From 7483379bcdc6c422eabb26376f9cbb5c0fc5a4d1 Mon Sep 17 00:00:00 2001 From: Anthony GAEREMYNCK <1@anthony.sh> Date: Sun, 2 Aug 2026 11:02:55 +0200 Subject: [PATCH] fix(seo): resolve theme SSR/hydration mismatch (React error #418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useTheme's initial state now always renders 'light' on both server and client's first render pass, matching exactly. The real stored/OS-preferred theme is applied afterward in an effect (runs only post-hydration), instead of computing it during the initializer, which returned different values between the server (no window) and a client whose actual localStorage/ matchMedia preference differed from the SSR default (e.g. dark-mode OS or a stored 'dark' preference) — a real hydration mismatch on the ThemeToggle's rendered icon and aria-pressed attribute, intermittent per visitor/system. --- frontend/src/hooks/useTheme.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/src/hooks/useTheme.js b/frontend/src/hooks/useTheme.js index a6fe80c..7787e07 100644 --- a/frontend/src/hooks/useTheme.js +++ b/frontend/src/hooks/useTheme.js @@ -2,15 +2,22 @@ import { useEffect, useState } from 'react'; const STORAGE_KEY = 'theme'; -function getInitialTheme() { - if (typeof window === 'undefined') return 'light'; +function readStoredOrPreferredTheme() { const stored = localStorage.getItem(STORAGE_KEY); if (stored === 'light' || stored === 'dark') return stored; return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } export function useTheme() { - const [theme, setTheme] = useState(getInitialTheme); + const [theme, setTheme] = useState('light'); + + useEffect(() => { + const preferred = readStoredOrPreferredTheme(); + if (preferred !== theme) { + setTheme(preferred); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); useEffect(() => { document.documentElement.setAttribute('data-theme', theme);