This is a classic hydration mismatch caused by async client-side initialization. The fix you found is the correct pattern:
The Pattern:
const [isReady, setIsReady] = useState(false)
useEffect(() => {
// Do async client-side work
i18n.changeLanguage(locale).then(() => setIsReady(true))
}, [locale])
if (!isReady) return null // or <Loading />
return children
Why it works:
useEffect only runs on the client (never during SSR)
Returning null until ready ensures server and client render the same thing initially
Once client is hydrated, the async work completes and re-renders with correct language
I actually built a tool called NeuroLint that automatically detects and fixes these hydration patterns in React/Next.js codebases. It uses AST transformations (no AI) to find issues like:
Direct localStorage/window access without guards
Missing typeof window !== "undefined" checks
Components that need client-side initialization wrapping
Check it out: https://neurolint.dev/