Next.js App Router: "Hydration failed because the initial UI does not match what was rendered on the server" when reading localStorage in useState
Body:
I'm using Next.js 14 (App Router). I want to store the user's theme preference in localStorage and read it on the first render, but I get a hydration error.
jsx
"use client";
import { useState } from "react";
export default function ThemeToggle() {
const [theme, setTheme] = useState(
localStorage.getItem("theme") || "light"
);
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Current theme: {theme}
</button>
);
}
Expected: The button shows the saved theme.
Actual: On a hard refresh I get this error:
ReferenceError: localStorage is not defined
and, after wrapping it in typeof window !== "undefined", this one instead:
Error: Hydration failed because the initial UI does not match what was rendered on the server.
What I tried:
Checking
typeof window !== "undefined"insideuseState, which fixed the first error but caused the hydration mismatch.Moving the code into
useEffect, but then the UI flashes the wrong theme for a moment.
What is the correct way to read localStorage on the first render without a hydration error or a flicker?