Diagnosing Astro v4 Island Hydration Failures: Symptoms, Causes, and Fixes
Markup renders but nothing is clickable? That's a failed island hydration in Astro v4. This diagnostic guide maps symptoms to causes — missing client directives, SSR-unsafe browser APIs, export mistakes — with ordered checks and fixes.
05 Jul 2026, 00:37 UTC

Your page renders fine — the markup is all there — but clicking a button does nothing, a dropdown never opens, or a counter stays frozen at zero. In an Astro v4 project, that pattern almost always means an island failed to hydrate: the server rendered the component's HTML, but the client-side JavaScript that makes it interactive never ran. This guide walks through how to recognize the failure, narrow down the cause, and apply the right fix.
Recognizing a hydration failure
Hydration is the process where Astro ships a component's JavaScript to the browser and attaches event handlers to the server-rendered HTML. When it fails, the symptoms are consistent:
- The component's markup appears in the page source, but no interactivity works.
- The browser console shows errors like
Hydration completed but contains mismatchesor module-loading failures for a chunk in_astro/. - The build log warns that a framework component was rendered without a
client:*directive, so no JavaScript was emitted for it. - In dev tools' Network tab, the component's JavaScript bundle is never requested after page load.
A quick confirmation: view the page source (not the live DOM). If the HTML is present there but the component never becomes interactive, you're dealing with hydration, not a rendering bug.
Cause and diagnostic table
| Finding | Likely cause | Where to confirm |
|---|---|---|
| No JS bundle requested for the component | Missing client:* directive on the component tag | The .astro file using the component; build warnings |
| Component renders as plain HTML in all cases | Component file lacks a default export (React/Vue/Svelte) | The component source file's export statement |
Console error referencing window or document during SSR | Component (or a library it imports) touches browser APIs at module scope during server rendering | Terminal running astro dev; browser console stack trace |
| Site is fully static output but hydration-dependent routes behave oddly | Adapter mismatch — e.g., expecting server features with adapter-static output | astro.config.mjs output and adapter settings |
| Hydration starts but throws a mismatch error | Server HTML differs from first client render (dates, random values, locale-dependent output) | Browser console mismatch warning; component's initial state logic |
Ordered checks
Work through these in order; each is cheap and eliminates a whole class of causes.
1. Confirm the client directive exists and matches your intent
Open the .astro file that uses the component and look for a directive such as:
---\nimport Counter from '../components/Counter.jsx';\n---\n<Counter client:load />Without a client:* directive, Astro renders the component to static HTML and ships no JavaScript — by design. Choose the directive that fits: client:load (immediately), client:idle (when the browser is idle), client:visible (when scrolled into view), or client:only=\"react\" (skip server rendering entirely). If the directive is present but interactivity is still dead, move on.
2. Verify the component has a default export
UI framework components in Astro must be the default export of their file. A named export like export function Counter() without export default will not be treated as an island component. Check the component file and fix the export if needed:
// Counter.jsx\nexport default function Counter() {\n // ...\n}3. Check for browser-only APIs at module or render scope
Islands are server-rendered first, so top-level code like window.addEventListener(...) or localStorage.getItem(...) runs in Node during SSR and throws. Run astro dev (no special permissions needed, run it in your project root) and watch the terminal for errors mentioning window is not defined or similar. The fix is to move that code into a lifecycle hook that only runs in the browser — for example useEffect in React — or guard it with if (typeof window !== 'undefined'). If the whole component is browser-only, use client:only with the framework name so Astro skips server rendering for it.
4. Inspect the built output
Run astro build in the project root, then look in dist/_astro/ for a JavaScript chunk corresponding to your component. If no chunk exists, the directive from step 1 isn't being applied — check for typos like client:load placed on a wrapper element instead of the component, or a conditional that prevents the component tag from rendering. If the chunk exists but is never loaded in the browser, check the browser console for a failed request (ad blockers and aggressive CSP headers can block module scripts).
5. Check for server/client HTML mismatches
If hydration starts but the console reports a mismatch, the server-rendered HTML differs from the component's first client render. Common culprits: Date.now(), Math.random(), user-agent sniffing, or locale formatting in initial state. Make the initial render deterministic and update dynamic values in an effect after mount.
6. Review adapter and output configuration
In astro.config.mjs, confirm the setup matches your deployment. With output: 'static' (the default, used with @astrojs/adapter-static-style deployments), islands still hydrate fine — but any route expecting server-side behavior will not. This guide assumes Astro v4 with static output or a mainstream adapter such as Vercel's; other adapters can impose their own constraints, so check the adapter's documentation if steps 1–5 come up clean.
Fixes tied to findings
- Missing directive: add the appropriate
client:*directive. Prefer the laziest option that meets the UX need (client:visiblefor below-the-fold widgets) to keep JS payload small. - Named export only: switch to
export default. - Browser API at SSR time: move the code into a mount-time hook, guard it, or use
client:only. - Missing chunk in dist: fix directive placement; rebuild and re-check
dist/_astro/. - Mismatch errors: make initial render deterministic; defer dynamic values to post-mount effects.
- Blocked scripts: relax CSP
script-srcto allow your origin's module scripts; test with extensions disabled.
Verifying the fix
After each change, verify rather than assume:
- Run
astro devand confirm the terminal shows no SSR errors and no missing-directive warnings. - In browser dev tools' Network tab, filter by JS and confirm the component's chunk loads with a 200 status after page load.
- In the console, confirm there are no hydration mismatch or module errors.
- Exercise the interaction (click, type, toggle) and confirm state changes in the live DOM.
When to escalate
Some causes sit outside this checklist. Escalate — to a minimal reproduction and an issue against the relevant integration or dependency — when:
- The failure only occurs with one third-party library, especially one that keeps global mutable state or isn't tree-shakable; try reproducing with a bare component that imports only that library.
- Hydration fails only in production builds but not in dev, which points at bundling/minification differences; test with
astro build && astro previewlocally to reproduce. - The component hydrates in one framework integration but not another with identical logic, suggesting an integration-level bug.
For a minimal reproduction, strip the page to a single island with a single client:load directive. If it hydrates, add pieces back until it breaks — the last added piece is your suspect. If even the bare island fails, the problem is project configuration, not the component.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.