Automatic pending state for data being fetched?

TLDR: Does anyone know if there exists a way to wrap variables in a sort of suspense wrapper that will trigger a loading indicator whenever it registers that its content is currently being fetched or re-fetched? 🧐 I know I could manually create a "pending" variable and set it to false/true before and after a fetch, however as the application scales the amount of different pending variables I would need grows and I don't want to store them all in objects (ex. loading = {title: true, paragraph: false, etc....}) 🔥 useFetch has a "status" return value which can be used to detect the status of a fetch, but this only works when the page is originally loaded and not on fetches triggered by user interaction (ex. clicking a fetch button) Does a loading wrapper exist or does anyone have an idea for how to implement one? Is there a way to check whether data within a component is currently being awaited? Example: <wrapper>{{ data }}</wrapper> The wrapper should detect when data inside of it is being awaited (just checking if data is null doesn't work because we might want to refetch data, and I don't want to set it to null each time before refetch) Any thoughts or suggestions would be much appreciated🙏
19 Replies
yosefe
yosefe4mo ago
Glad to help you with this. Can you please share your current code and what sort it you’re trying to fetch and what sort of data do you want to fetch in the
Reinier Kaper
Reinier Kaper4mo ago
The data fetching composables (useFetch, useAsyncData), already have a status property you can leverage
Reinier Kaper
Reinier Kaper4mo ago
Nuxt
useFetch · Nuxt Composables
Fetch data from an API endpoint with an SSR-friendly composable.
AxelSorensen
AxelSorensenOP4mo ago
Right, but it seems to me that useFetch() is only used on page load and not on user interaction. Screenshot is from https://nuxt.com/docs/getting-started/data-fetching
Nuxt
Data fetching · Get Started with Nuxt
Nuxt provides composables to handle data fetching within your application.
No description
Reinier Kaper
Reinier Kaper4mo ago
No that's not the case. You can use useFetch in all kinds of scenarios, also on user interaction
AxelSorensen
AxelSorensenOP4mo ago
But this doesn't solve the issue of having multiple pending variables if I have 100 different fields that I need to fetch with each their "loading" fallback text.
Reinier Kaper
Reinier Kaper4mo ago
What issue do you run into with that? (You can rename the variables btw: const { state: field1State } = useFetch())
AxelSorensen
AxelSorensenOP4mo ago
Right, but what I would really love is to automatically detect that data is being awaited and whenever it is, display some arbitrary loading indicator. I know this can be done with <Suspense/> in React for example, but again this only applies to first time the data is being fetched it seems Take a look at my example in my post: I want a wrapper around any data, that will show the data if it is fetched or default to a loading indicator. This would mean that in my main app I could handle all the data fetching and as long as i wrap my data in this wrapper: <wrapper>{{data}}</wrapper> or pass it as a prop <wrapper :data="data"/> Would show "loading" or "data" depending on the fetchstate of the given data
Reinier Kaper
Reinier Kaper4mo ago
<wrapper :status="status">{{ data }}</wrapper> then handle the status logic in the wrapper component?
Mähh
Mähh4mo ago
what about a custom $fetch wrapper? (btw. your are right: useFetch only should be used for getting data, not for update or create requests, for that $fetch should be used, thats because of internal caching logic behind useAsyncData / useFetch i guess) Anyways.
export const useRequestState () => {
const pendingCount = useState('requests-pending', () => 0)
const pending = computed(() => pendingRequests.value > 0)

return { pendingCount, pending }
}
export const useRequestState () => {
const pendingCount = useState('requests-pending', () => 0)
const pending = computed(() => pendingRequests.value > 0)

return { pendingCount, pending }
}
export const useMyCustomFetch = (request, opts) => {
const {pendingCount} = useRequestState()

return useFetch(request, {
...opts,
onRequest() {
++pendingCount.value
},
onResponseError() {
--pendingCount.value
},
onResponse() {
--pendingCount.value
}
})
}
export const useMyCustomFetch = (request, opts) => {
const {pendingCount} = useRequestState()

return useFetch(request, {
...opts,
onRequest() {
++pendingCount.value
},
onResponseError() {
--pendingCount.value
},
onResponse() {
--pendingCount.value
}
})
}
for $fetch the same could be used. e.g. implement a global $myFetch via plugin, or just using a composable like:
export const $myFetch = () => {
const {pendingCount} = useRequestState()

return $fetch.create({
onRequest() {
++pendingCount.value
},
onResponseError() {
--pendingCount.value
},
onResponse() {
--pendingCount.value
}
})
}
export const $myFetch = () => {
const {pendingCount} = useRequestState()

return $fetch.create({
onRequest() {
++pendingCount.value
},
onResponseError() {
--pendingCount.value
},
onResponse() {
--pendingCount.value
}
})
}
Which can be called in components
$myFetch()('/foo', { method: 'POST' })
$myFetch()('/foo', { method: 'POST' })
plugin would be more elegant way to inject $myFetch globally. Anyways, i think you got the point. Have a state which is aware of pending requests in you application, by intercept the requests by its core methods 😄
Mähh
Mähh4mo ago
Checked the source code: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/composables/asyncData.ts#L251C5-L257C6
nuxtApp._asyncData[key] = {
data: _ref(typeof cachedData !== 'undefined' ? cachedData : options.default!()),
pending: ref(!hasCachedData()),
error: toRef(nuxtApp.payload._errors, key),
status: ref('idle'),
_default: options.default!,
}
nuxtApp._asyncData[key] = {
data: _ref(typeof cachedData !== 'undefined' ? cachedData : options.default!()),
pending: ref(!hasCachedData()),
error: toRef(nuxtApp.payload._errors, key),
status: ref('idle'),
_default: options.default!,
}
This is what happens behind useFetch and useAsyncData you could use that to get all the status references and return a bool to know if any useFetch or useAsyncData is pending / refreshing. For the custom $fetch here is an example for the plugin way: https://nuxt.com/docs/guide/recipes/custom-usefetch
Nuxt
Custom useFetch in Nuxt · Recipes
How to create a custom fetcher for calling your external API in Nuxt 3.
GitHub
nuxt/packages/nuxt/src/app/composables/asyncData.ts at main · nuxt/...
The Intuitive Vue Framework. Contribute to nuxt/nuxt development by creating an account on GitHub.
AxelSorensen
AxelSorensenOP4mo ago
Thanks a lot! This looks promising Do you think that a variable that is currently being assigned to an awaited fetch has some tell? For example:
<template>
<div>
<button @click="handleClick">Click me</button>
<p v-if="message.pending">Loading</p>
<p v-else>{{ message }}</p>
</div>
</template>

<script setup>
import { ref } from 'vue';

// Create a reactive reference to hold the message
const message = ref('');

// Define the async function
const fetchMessage = async () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Hello World');
}, 1000);
});
};

// Handle the button click event
const handleClick = async () => {
message.value = await fetchMessage();
};
</script>
<template>
<div>
<button @click="handleClick">Click me</button>
<p v-if="message.pending">Loading</p>
<p v-else>{{ message }}</p>
</div>
</template>

<script setup>
import { ref } from 'vue';

// Create a reactive reference to hold the message
const message = ref('');

// Define the async function
const fetchMessage = async () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Hello World');
}, 1000);
});
};

// Handle the button click event
const handleClick = async () => {
message.value = await fetchMessage();
};
</script>
I would like the message variable to reveal a pending state when awaiting assignment. I tried checking whether my variable == Promise, but this didn't work
Mähh
Mähh4mo ago
Do i get it correctly, you wan't some logic like the default from useAsyncData where you don't only have the Promise, instead also can access the fields like data, error, status and so on without the Promise is done? you could do it like nuxt:
const message = ref({ pending: false, data: null })

const fetchMessage = async () => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve({ pending: false, data: 'My message' });
}, 1000);
});

return Object.assign(promise, { pending: true, data: null })
};

message.value = await fetchMessage()
const message = ref({ pending: false, data: null })

const fetchMessage = async () => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve({ pending: false, data: 'My message' });
}, 1000);
});

return Object.assign(promise, { pending: true, data: null })
};

message.value = await fetchMessage()
its similar like in the useAsyncData https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/composables/asyncData.ts#L395-L398
const asyncDataPromise = Promise.resolve(nuxtApp._asyncDataPromises[key]).then(() => asyncData) as AsyncData<ResT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>

Object.assign(asyncDataPromise, asyncData)

return asyncDataPromise
const asyncDataPromise = Promise.resolve(nuxtApp._asyncDataPromises[key]).then(() => asyncData) as AsyncData<ResT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>

Object.assign(asyncDataPromise, asyncData)

return asyncDataPromise
but there the data and pending is a reference / reactive prop itself. So a better example would be:
const fetchMessage = async () => {
const myAsyncDataObj = { pending: ref(true), data: ref(null) }

const promise = new Promise((resolve) => {
setTimeout(() => {
myAsyncDataObj.pending.value.value = false
myAsyncDataObj.pending.data.value = 'Hello world'

return myAsyncData
}, 1000);
});

return Object.assign(promise, myAsyncDataObj)
}
const fetchMessage = async () => {
const myAsyncDataObj = { pending: ref(true), data: ref(null) }

const promise = new Promise((resolve) => {
setTimeout(() => {
myAsyncDataObj.pending.value.value = false
myAsyncDataObj.pending.data.value = 'Hello world'

return myAsyncData
}, 1000);
});

return Object.assign(promise, myAsyncDataObj)
}
Which could be accessed by a similar logic
const {data: message, pending} = await fetchMessage()
const {data: message, pending} = await fetchMessage()
PS: Code is not tested, maybe it need adjustements 🙂
AxelSorensen
AxelSorensenOP4mo ago
Thanks @Mähh I would really like to not have to initialize my variables as objects with pending and data keys as this clutters the code. My dream scenario is simply that I can somehow check if a variable is currently in a "fetching state" For example imagine:
let data = 'Hello'
data = await someFunctionThatReturnsWorldAfter1Second
let data = 'Hello'
data = await someFunctionThatReturnsWorldAfter1Second
I wonder whether a value that is currently being assigned with await has some kind of metadata associated with it that I could query to trigger my loading animation.
Reinier Kaper
Reinier Kaper4mo ago
Isn't that literally what status already does?
AxelSorensen
AxelSorensenOP4mo ago
Yes but can you show me an example of using "status" when fetching something on click without having to store it in a variable Because if I need to store it in a variable I need different variables for each field. This is what I'm trying to avoid
Reinier Kaper
Reinier Kaper4mo ago
Sure, sec
Reinier Kaper
Reinier Kaper4mo ago
Unless you somehow use the interceptors to track a "global" count of fetch requests, you'll have to do some manual tracking :https://stackblitz.com/edit/github-tfsw15?file=app.vue
StackBlitz
Nuxt - Starter - StackBlitz
Create a new Nuxt project, module, layer or start from a theme with our collection of starters.
Reinier Kaper
Reinier Kaper4mo ago
This comment has a good solution to doing it automatically: https://discord.com/channels/473401852243869706/1263445640537702421/1263513911714381976 But the caveat is that this will track every fetch request, not just the ones you might want to track "locally"
Want results from more Discord servers?
Add your server