nuxt icon indicating copy to clipboard operation
nuxt copied to clipboard

on initial client side load with `server: false`, useFetch does not await data fetching

Open siddharth223437 opened this issue 2 years ago • 22 comments

Environment

Operating System: Darwin Node Version: v17.2.0 Nuxt Version: 3.0.0-27491748.3186841 Package Manager: [email protected] Builder: vite User Config: - Runtime Modules: - Build Modules: -

Reproduction

https://codesandbox.io/s/unruffled-rain-zzuixq?file=/app.vue

Describe the bug

When using useFetch composable inside setup method or inside any lifecycle hook then it directly shows error. It does not even hit the backend api.

Additional context

No response

Logs

RefImpl {
  __v_isShallow: false,
  dep: undefined,
  __v_isRef: true,
  _rawValue: null,
  _value: null
}
RefImpl {
  __v_isShallow: false,
  dep: undefined,
  __v_isRef: true,
  _rawValue: null,
  _value: null
}

siddharth223437 avatar Apr 23 '22 03:04 siddharth223437

I've noticed this issue too. Here are a few other pieces of information that I've noticed that may be helpful in determining the problem:

  • this issue happens when the page is refreshed
  • the issue does not happen with useFetch if the call is made from a user action, like a button click triggering the api call
  • if I use $fetch instead of useFetch, the call is successful, even on page refresh
  • the useFetch call returns immediately, even with await preceding the call

bbrn avatar Apr 23 '22 04:04 bbrn

Does not reproduce on local repo with nuxt-3.0.0-rc.1 nor StackBlitz...

Aareksio avatar Apr 23 '22 11:04 Aareksio

Seems to me like CodeSandbox is not executing the client-side JavaScript at all. May have something to do with the error below:

image

Aareksio avatar Apr 23 '22 11:04 Aareksio

There is an error on server side, see the terminal output. For the first time useFetch is trying to execute api call inside server.

Screen Shot 2022-04-23 at 10 57 54 AM

siddharth223437 avatar Apr 23 '22 16:04 siddharth223437

This is not an error, this is how empty data looks when passed to console.log.


It is NOT trying to execute the API call, but the returns empty (null) value to allow for other logic to run, eg:

<template>
  <h1>Today is {{ date }}</h1>
  <p>Quote of the day is: {{ quote }}</p>
</template>

<script setup>
  const { data: quote } = await useFetch('https://random-quote-api.com', { server: false })
  const date = (new Date()).toLocaleString()
</script>

useFetch resolves without making any request, to let the following line (const date = ...) to execute. When the code is executed again on client side, useFetch will block until the request is finished.

Aareksio avatar Apr 23 '22 16:04 Aareksio

ok, but useFetch is not getting executed on client side. i am getting null output. Though i can add a timeout as a workaround but its not ideal on every page.

Also in your example you are displaying quote thats the reason you are able to see output but in my case i don't want to just display data rather perform some logic and then display result. In this case if output is null then how can i perform my logic ?

Also, Response is not empty. Just grab that api and run on your browser, you will then see the response. Response should look like :

{ "message": "https://images.dog.ceo/breeds/retriever-flatcoated/n02099267_4546.jpg", "status": "success" }

If you add a button and execute useFetch then you will the response.

Attached screenshot is from chrome developer tool.

Screen Shot 2022-04-23 at 12 15 20 PM

siddharth223437 avatar Apr 23 '22 17:04 siddharth223437

I'm getting this as well since updating to the release candidate (although my previous nuxt3 version was a few months old) - for me, useLazyFetch data is undefined on the client side unless I do something like onMounted(() => refresh()).

Looking in the console at window.__NUXT__.data, the correct payload is there but not being accessed for some reason. Like, the SSR'd HTML coming from the server is correct but once hydration happens the data becomes undefined, which then causes a Hydration node mismatch warning.

Update: refactoring useLazyFetch to useLazyAsyncData (with a preset key and just a simple () => $fetch(url) as the handler) resolves the issue - perhaps this indicates that the issue is to do with the auto-generated key in useLazyFetch?

2nd update: Turns out my issue is just nuxt/nuxt.js#13790.

matthewpull avatar Apr 23 '22 17:04 matthewpull

@siddharth223437

You are using server: false, which means you must add fallback for server yourself:

<template>
  <div>
    <h1>Random Dog</h1>

    <div v-if="!pending">
      <!-- This is NOT rendered on server, only renders on client after API call completes -->
      <img src="dogImageSrouce"/>
    </div>
  </div>
</template>

<script setup>
const { data, pending } = await useFetch('https://dog-pictures.com/random', { server: false })
const dogImageSrouce = computed(() => data.value ? data.value.message : null)
</script>

Aareksio avatar Apr 23 '22 17:04 Aareksio

@Aareksio you can try removing server:false. You will get the same error log.

siddharth223437 avatar Apr 23 '22 17:04 siddharth223437

@Aareksio you can try removing server:false. You will get the same error log.

@siddharth223437 https://stackblitz.com/edit/github-8lgefy?file=app.vue

The message is NOT an error, you are explicitly logging data, this is how data looks on server. This is expected behavior. 🤔

Aareksio avatar Apr 23 '22 17:04 Aareksio

If I want to perform some logic on returned response then how can I achieve that if nuxt is not blocking call on client side ?

siddharth223437 avatar Apr 23 '22 17:04 siddharth223437

Please check the example above, it shows a very basic way to protect both template and script logic.

It is not blocking on server, not client. You explicitly pass server: false. If you want it to block, remove the option.


Edit to avoid spamming this issue further. I find your problem's root in not understanding how useAsyncData and useFetch fundamentally work in nuxt 3, along with testing on CodeSandbox which is broken. Please see final example: https://stackblitz.com/edit/github-8lgefy-j4ssaw?file=app.vue

The behavior may be unexpected, but as far as I am aware, it works as intended.

Aareksio avatar Apr 23 '22 17:04 Aareksio

@Aareksio You are not getting my point, i don't want to just display response. Please see my below code

<template>
  <div>don't display response from api</div>
</template>

<script setup>
const url = 'https://dog.ceo/api/breeds/image/random';
//const options = { server: false };

const { data } = await useFetch(url);
//i want to do something like
// if(data.value){
// perform some logic, at this point i am not getting any result back on client side
//}
</script>

siddharth223437 avatar Apr 23 '22 18:04 siddharth223437

I agree with @siddharth223437. The main issue for me is that the call is not blocked and the response never gets updated with the payload. So, the await is pointless. For me, the call is made to the backend but just doesn't get blocked and then the only way to get the data is to watch data. This ends up behaving like useLazyFetch, which isn't what is expected.

bbrn avatar Apr 23 '22 20:04 bbrn

On initial load, with server: false, we can't await the data fetching before hydration is complete or we'll actually cause a hydration error (because the client-side rendered state will be different by this point compared to the SSR HTML).

Note that on client-side navigation we do await the fetching as you expect.

Here's the code:

https://github.com/nuxt/framework/blob/4712e994112a97feba447b3edb4afb65e968ed63/packages/nuxt/src/app/composables/asyncData.ts#L136-L154

I agree that this is somewhat counter-intuitive. If you have any suggestions for improving the initial load experience, please do let me know. But for now you will need to handle a pending state if you have set server: false.

danielroe avatar Apr 27 '22 10:04 danielroe

Same problem when nuxt is set to ssr: false: await is not waiting for useFetch() to actually fetch the data.

mario-neuhold avatar May 03 '22 21:05 mario-neuhold

@mario-neuhold I ended up creating composable which is a wrapper on $fetch

import { useRuntimeConfig } from 'nuxt/app' import {$fetch} from "ohmyfetch";

composable/useMyFetch.ts

export const useMyFetch = async (req: string, opts?) => {
  const config = useRuntimeConfig()

  return $fetch(req,{
    ...opts,
    baseURL: config.aisAuth.client.baseUrl,
    headers: {
      Accept: 'application/json',
    },
    credentials: 'include',
    async onResponse({ request, response, options }) {
      if(response.status === 401){
          // do something on 401 response
      }

    }
  }).catch((err) => ({error: err}))
}

Then you can use useMyFetch inside your .vue file

async function myFunc(){
    const resp = await useMyFetch('url')
}

siddharth223437 avatar May 04 '22 15:05 siddharth223437

I used the immediate option.

const fetch = useFetch(url, { immediate: false })
await fetch.execute({ _initial: true })

alfa-jpn avatar Nov 03 '22 02:11 alfa-jpn

Thanks, @alfa-jpn that technique solved this for me (for now!).

I'll add that in our case, we aren't setting server: false explicitly, but instead are calling useFetch() within onMounted(), which perhaps results in the same behavior. Our application was running on rc.6 for a long time, where calling fetch in onMounted() was working fine, and we are just now upgrading to 3.0 stable when this issue surfaced.

@danielroe is there a better way to wait until hydration is finished before making the fetch? In our application we are loading a lot of data on some pages so are splitting the fetching between server and client side to get a better initial response time while still getting the benefits from the most critical data fetched server side.

cyruscollier avatar Dec 12 '22 21:12 cyruscollier

Ah. If you are calling something within a method, you should almost certainly use $fetch directly rather than useFetch, which is only meant to be called in the setup method.

danielroe avatar Dec 12 '22 21:12 danielroe

The documentation says that useFetch() can also be used in lifecycle hooks. I think my confusion (and perhaps others) is around the best practice that composable functions should only be called directly at the top level of the setup call stack, not within helper functions or other shared code not directly scoped to setup. Is that right?

cyruscollier avatar Dec 12 '22 22:12 cyruscollier

@cyruscollier Yes, that's right. Or within a function immediately invoked within the setup function (a composable).

danielroe avatar Dec 12 '22 22:12 danielroe

I want to document this issue.

mihul87 avatar Aug 05 '23 20:08 mihul87

Although it's already mentioned in documents, I would like to share some ideas for clearer logic.

In this case I use watch() to watch whether status is updated, and I specify immediate to make sure this code will be run at least once. So no matter what ssr and server are, it should be able to process API response, correctly.

const res = await useFetch('https://nekos.best/api/v2/neko', {server: false});
watch(res.status, () => {
  if (res.status.value === 'success') {
    const v = res.data.value;
    // ...
  }
}, {immediate: true});

I am not sure if there are any other side effects, but it works on my project so far.

gslin avatar Nov 21 '23 22:11 gslin