openapi-ts icon indicating copy to clipboard operation
openapi-ts copied to clipboard

[nuxt-client] useFetch/useAsyncData not obeying await, causing hydration mismatch errors

Open lrstanley opened this issue 8 months ago • 5 comments

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

lrstanley avatar Mar 28 '25 02:03 lrstanley

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 😀🥲

mrlubos avatar Mar 28 '25 02:03 mrlubos

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.

lrstanley avatar Mar 28 '25 03:03 lrstanley

I've done some further testing, and I've found some useful info:

  • for some reason, things like getPetById don'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 responseValidator via 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.

lrstanley avatar May 09 '25 08:05 lrstanley

If you re-implement the same logic without using Hey API, are you able to narrow down the problem?

mrlubos avatar May 09 '25 09:05 mrlubos