[nuxt-client] useFetch/useAsyncData not obeying await, causing hydration mismatch errors
Description
I think this has been a bug since the initial implementation of the nuxt module (not relating to the nuxt client), however, I didn't realize the problems I was having were related to the nuxt client (bet you're loving adding nuxt support 😄). I had all sorts of hydration errors through my app anywhere I was invoking APIs. However, Nuxt hydration errors can be tough to troubleshoot, so it took me a while to figure out this issue.
Using the following example for my site:
const {
data: posts,
error,
status,
} = await listPosts({
composable: "useFetch",
query: {
"slug.eq": "meet-bubblezone",
},
})
console.log(posts.value)
// SSR: outputs the proper object response.
// Client: outputs undefined.
Initially, on client with hydration, posts.value is undefined. After a few ms on client, posts.value will have the appropriate data, but not in the initial immediate request, leading me to believe that await is not being obeyed, and/or somehow isn't being passed down to the actual request through the chain of function calls.
Doing the same thing with native useFetch:
const {
data: posts,
error,
status,
} = await useFetch<PostList>(`http://localhost:8080/-/posts?slug.eq=meet-bubblezone`)
console.log(posts.value)
// SSR: outputs the proper object response.
// Client: outputs the proper object response.
Worth noting that when using something like $fetch, await works as expected, though this is obviously not ideal as it's only invoking client-side. useAsyncData acts the same as useFetch.
This is an example of one of those hydration errors (and to make it easier for others to search and find this issue):
[Vue warn]: Hydration node mismatch:
- rendered on server:
<div data-v-31a6324b=""> <empty string>
- expected on client: Symbol("v-cmt")
at <[slug] onVnodeUnmounted=fn<onVnodeUnmounted> ref=Ref< undefined > >
at <RouteProvider key="/p/meet-bubblezone()" vnode=
Object { __v_isVNode: true, __v_skip: true, type: {…}, props: {…}, key: null, ref: {…}, scopeId: null, slotScopeIds: null, children: null, component: null, … }
route=
Object { fullPath: "/p/meet-bubblezone", hash: "", query: {}, name: "p-slug", path: "/p/meet-bubblezone", params: {…}, matched: (1) […], meta: Proxy, redirectedFrom: undefined, href: "/p/meet-bubblezone" }
... >
at <RouterView name=undefined route=undefined >
at <NuxtPage>
at <Default ref=Ref< undefined > >
at <AsyncComponentWrapper ref=Ref< undefined > >
at <LayoutLoader key="default" layoutProps=
Object { ref: {…} }
name="default" >
at <NuxtLayoutProvider layoutProps=
Object { ref: {…} }
key="default" name="default" ... >
at <NuxtLayout>
at <ToastProvider swipe-direction="right" duration=5000 >
at <Toaster key=0 >
at <TooltipProvider>
at <ConfigProvider use-id=fn<use-id> dir=undefined locale=undefined >
at <App>
at <App key=4 >
at <NuxtRoot>
Reproducible example or configuration
https://stackblitz.com/edit/nuxt-examples-3jsj37yn?file=components%2Fexample.vue
OpenAPI specification (optional)
No response
System information (optional)
@hey-api/client-nuxt 0.3.1
@hey-api/nuxt 0.1.3
@hey-api/openapi-ts 0.64.15
Sorry, I'd like to see a reproducible example because I can't imagine why this would be happening.
PS. The answer to your question is yes 😀🥲
Reproducible example:
https://stackblitz.com/edit/nuxt-examples-3jsj37yn?file=components%2Fexample.vue
The big thing to look at with that example is:

aaaaand:

Every once in awhile, the initial load on HMR does show that it has data, but I think that's purely a race condition in that it just happens to return quick enough because initial HMR loads can be very slow. Any tests after HMR has "stabilized" do not have the initial await returning data.
I didn't want to use the same exact endpoint/query in both, because Nuxt & specifically useFetch deduplicates requests behind the scenes, but there should be no difference between these two endpoints.
I've done some further testing, and I've found some useful info:
- for some reason, things like
getPetByIddon't have the same problem. I originally thought that meant it was something in the query serialization logic, but it wasn't. - in my reproduction, I have zod as well as transformers enabled. toggling zod, and the issue goes away (as well as disabling the invocation of
responseValidatorvia commenting it out or similar, inside@hey-api/client-nuxt). - I've confirmed that it's not a case of zod somehow mutating the original object (json serialized and unserialized when passing it to zod, no difference).
Almost makes me think there is something with the findPetsByStatus schema, vs the getPetById schema, that is different, that zod is tripping up on, and maybe doing something in another tick? idk. I still think that it's somehow related to promise logic, because I can't think of any other scenario that would result in the same issues.
If you re-implement the same logic without using Hey API, are you able to narrow down the problem?