2026-09-04 · 1 min read
The header that threw itself away
One line in a shim was silently destroying an island on every page but one.
Every page except the home page was quietly rendering the header twice: once on the server, wrong, and once on the client, right. React logged a hydration mismatch and discarded the whole island — and because the uncaught error landed mid-hydration, every island after it on the page stayed dead too.
The cause
The site reuses React components ported from Next.js, with small shims standing in for the framework’s APIs. The routing shim looked reasonable:
export function usePathname(): string {
const [pathname, setPathname] = useState(() =>
typeof window !== "undefined" ? window.location.pathname : "/"
);
// ...
}
On the server there is no window, so it returns "/" and the header renders
its home-page branch. On the client window exists, so the very first render
already knows the real path and renders the breadcrumb instead. Two different
trees, every time.
The fix
The page already knows its own path, so pass it in:
<Header client:load pathname={Astro.url.pathname} />
Astro serialises island props into the HTML, so both renders start from the same value. The effect that listens for view transitions still keeps it current afterwards.
What it cost to find
The symptom was “buttons don’t work on some pages”, which points at the buttons. The actual error named a component three levels up. Worth remembering that a hydration failure is not local — it takes out everything downstream of it.