Diagnosing Nuxt 3 SSR Hydration Mismatches with useFetch
Learn how to spot, diagnose, and fix Nuxt 3 SSR hydration mismatches when using the useFetch composable.
01 Jun 2026, 04:22 UTC

Recognizable condition
When a Nuxt 3 page loads, you see a console warning similar to "Hydration completed but contains mismatches", and the UI may flicker or show missing content after the initial HTML is painted. The server rendered one version of the markup; Vue's client-side hydration produced another and had to patch or discard parts of the DOM.
Common causes and diagnostic clues
| Cause | What to look for |
|---|---|
| useFetch executed only on the client | The composable is called inside a client-only guard or after the component mounts, so the server never receives the same data. |
| Mutable state changed after render | Data returned from useFetch is later mutated (for example, pushing to an array), causing the client DOM to diverge from the server-rendered markup. |
| Async timing differences | The server resolves the fetch before the client, or the client uses a different request (for example, one that depends on window size), leading to divergent payloads. |
Ordered checks
- Verify placement. Open the component that uses
useFetchand ensure the call is insidesetup()(or a composable invoked fromsetup()) with noif (process.client)wrapper around it. - Check serializability. Confirm the value returned from
useFetchcontains only JSON-serializable types — nowindow,document, DOM nodes, class instances, or functions. Inspect the returned value in the browser console; non-plain objects are a red flag because Nuxt embeds the payload in the page as JSON. - Compare server and client payloads. In Chrome DevTools, copy the root element's outer HTML before interacting with the page — that is the server-rendered snapshot. Then, after hydration, use the Nuxt DevTools Hydration panel (or Vue DevTools) to capture the client-rendered DOM and diff the two. The first differing node usually points at the responsible component.
Fixes tied to findings
Move data fetching into a shared composable using useAsyncData. This guarantees the call runs on the server and that the result is serialized into the payload for the client to reuse, instead of refetching with possibly different results:
// composables/usePosts.js
export function usePosts () {
return useAsyncData('posts', () => $fetch('/api/posts'))
}
// inside a page or component's script setup block:
// const { data: posts } = usePosts()Guard client-only accesses. If you need browser-specific APIs, wrap them in if (process.client) and keep the fetched data out of that block:
if (process.client) {
// safe to use window.innerWidth etc.
const width = window.innerWidth
}Key list items with stable IDs. When rendering arrays from fetched data, bind a unique, stable key (for example :key="item.id") so Vue does not reorder or reuse elements during hydration.
Force eager hydration for a problematic component. If you use lazy-hydration wrappers, temporarily render the suspect component eagerly (for example via Nuxt's delayed-hydration controls or by removing the lazy wrapper) to see whether the mismatch is timing-related.
Escalation criteria
If the mismatch warning persists after the checks and fixes above:
- Launch Nuxt DevTools (
npx nuxi devtools enable, then restart the dev server) and open the Hydration panel to see the exact diff. - Inspect the Nitro server logs (dev terminal output, or your hosting platform's log stream in production) for errors thrown while
useFetchresolved on the server. - As a temporary workaround, set
ssr: falsefor the affected route viarouteRulesinnuxt.config.tsto render that page purely on the client while you investigate. Do not disable SSR globally — it removes SEO and first-paint benefits for content-rich pages. - If the data does not change per request, consider prerendering the route (
routeRules: { '/': { prerender: true } }) so server and client always share the same static payload.
Limitations and verification
These steps cover the most common hydration mismatches caused by useFetch. They do not address third-party plugins that mutate the DOM outside Vue's reconciliation, or custom directives that modify elements after server render. To verify a fix:
- Run the dev server with Nuxt DevTools and confirm the Hydration panel shows no warnings after a full page reload.
- Build with
nuxt buildand start withnuxt start; view the page source and confirm the expected data appears in the initial HTML. - Deploy to a preview environment and run Lighthouse; confirm SEO-critical content is present in the loaded HTML, not only after JavaScript executes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.