await callOnce() blocks navigation

In my nuxt app i replaced:
const { status } = useAsyncData(() => store.fetch())
const { status } = useAsyncData(() => store.fetch())
with
await callOnce(() => store.fetch(), { mode: 'navigation' })
await callOnce(() => store.fetch(), { mode: 'navigation' })
in order to keep SSR while having a re-fetch when navigating to that page. Now the issue becomes that i have to manually implement the status ref with pending states on all components where i use callOnce and all pages become blocked on navigation. What would the correct approach be here?
11 Replies
kapa.ai
kapa.ai3d ago
Hi @SaltTheWound. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- Based on the information provided in the knowledge sources, I can offer some insights and suggestions regarding your issue with callOnce blocking navigation in your Nuxt app. The callOnce utility is designed to execute a given function or block of code only once during specific scenarios, such as server-side rendering or client-side navigation. However, it's important to note that callOnce doesn't return anything and is not meant for data fetching during SSR. For your use case, where you want to keep SSR while re-fetching data on navigation, the recommended approach would be to use useAsyncData or useFetch instead of callOnce. These composables are specifically designed for data fetching and provide better control over the loading states and navigation behavior. Here's a suggested approach: 1. Use useLazyAsyncData instead of callOnce:
const { status, data } = useLazyAsyncData(() => store.fetch(), {
watch: [/* add any reactive dependencies here */]
})
const { status, data } = useLazyAsyncData(() => store.fetch(), {
watch: [/* add any reactive dependencies here */]
})
This approach offers several advantages: 1. It allows for lazy loading, which means navigation won't be blocked. 2. It provides a status ref out of the box, so you don't need to implement it manually. 3. The watch option allows you to automatically re-run the fetch when certain dependencies change, which can be useful for navigation scenarios. By using useLazyAsyncData, you can achieve your goal of keeping SSR while allowing re-fetches on navigation, without blocking the navigation or manually implementing status handling. Remember to handle the pending state in your component's template:
<template>
<div v-if="status === 'pending'">
Loading...
</div>
<div v-else>
<!-- Your content here -->
</div>
</template>
<template>
<div v-if="status === 'pending'">
Loading...
</div>
<div v-else>
<!-- Your content here -->
</div>
</template>
This approach is more in line with Nuxt's data fetching patterns and should resolve the issues you're experiencing with callOnce. Source: Docs > Api > Composables > useLazyAsyncData Source: Docs > Getting-started > Data Fetching > Lazy ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3d ago
<@210839019674796033> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
SaltTheWound
SaltTheWoundOP3d ago
@kapa.ai useAsyncData is not meant to have side-effects though and the recommended way in the pinia docs is:
<script setup>
const store = useStore()
// we could also extract the data, but it's already present in the store
await callOnce('user', () => store.fetchUser())
</script>
<script setup>
const store = useStore()
// we could also extract the data, but it's already present in the store
await callOnce('user', () => store.fetchUser())
</script>
kapa.ai
kapa.ai3d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- You're absolutely correct, and I apologize for the confusion in my previous answer. You've raised an important point about the proper use of useAsyncData and callOnce, especially when working with Pinia stores. Let's clarify this based on the latest information from the Nuxt documentation. You're right that useAsyncData is not meant for triggering side effects like calling Pinia actions. The Nuxt documentation has been updated to specifically address this issue. According to the Docs > Getting-started > Data Fetching > useAsyncData section:
`useAsyncData` is for fetching and caching data, not triggering side effects like calling Pinia actions, as this can cause unintended behavior such as repeated executions with nullish values. If you need to trigger side effects, use the [`callOnce`](<https://nuxt.com/docs/api/utils/call-once>) utility to do so.
`useAsyncData` is for fetching and caching data, not triggering side effects like calling Pinia actions, as this can cause unintended behavior such as repeated executions with nullish values. If you need to trigger side effects, use the [`callOnce`](<https://nuxt.com/docs/api/utils/call-once>) utility to do so.
The example you provided from the Pinia docs using callOnce is indeed the recommended approach for this scenario. Here's why this is the correct way: 1. callOnce is designed to execute a function only once, which is perfect for initializing store data. 2. It avoids the potential issues that can occur when using useAsyncData with store actions, such as unintended re-executions. 3. It allows you to trigger side effects (like store actions) safely without interfering with Nuxt's data fetching system. Your code example is correct:
<script setup>
const store = useStore()
await callOnce('user', () => store.fetchUser())
</script>
<script setup>
const store = useStore()
await callOnce('user', () => store.fetchUser())
</script>
This approach ensures that the fetchUser action is called only once, either during server-side rendering or client-side navigation, depending on how you configure it. If you need to re-fetch on navigation, you can use the navigation mode of callOnce, as introduced in Nuxt v3.15:
await callOnce(() => store.fetchUser(), { mode: 'navigation' })
await callOnce(() => store.fetchUser(), { mode: 'navigation' })
This will run the function once initially and then on every client-side navigation. For handling loading states, you would need to manage this within your Pinia store or component, as callOnce doesn't provide a built-in status indicator like useAsyncData does. Thank you for bringing this up. It's a great reminder of the importance of using the right tool for the job in Nuxt applications, especially when working with state management libraries like Pinia. Source: Docs > Api > Utils > callOnce Source: Docs > Getting-started > Data Fetching ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3d ago
<@210839019674796033> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
SaltTheWound
SaltTheWoundOP3d ago
@kapa.ai Now how do i avoid the page being blocked by the await callOnce on navigation.
kapa.ai
kapa.ai3d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- To avoid blocking the page navigation with await callOnce, you can leverage Nuxt's built-in features for handling asynchronous operations during navigation. Here's an approach you can take: 1. Use callOnce without awaiting it in the component's setup function:
<script setup>
const store = useStore()

callOnce(() => store.fetchUser(), { mode: 'navigation' })
</script>
<script setup>
const store = useStore()

callOnce(() => store.fetchUser(), { mode: 'navigation' })
</script>
2. Handle the loading state in your store or component. Since callOnce doesn't provide a built-in status indicator, you'll need to manage this yourself. For example, in your Pinia store:
export const useStore = defineStore('user', {
state: () => ({
user: null,
loading: false
}),
actions: {
async fetchUser() {
this.loading = true
try {
// Fetch user data
this.user = await $fetch('/api/user')
} finally {
this.loading = false
}
}
}
})
export const useStore = defineStore('user', {
state: () => ({
user: null,
loading: false
}),
actions: {
async fetchUser() {
this.loading = true
try {
// Fetch user data
this.user = await $fetch('/api/user')
} finally {
this.loading = false
}
}
}
})
3. In your component, use the store's loading state to show a loading indicator:
<template>
<div v-if="store.loading">Loading...</div>
<div v-else>
<!-- User data display -->
</div>
</template>

<script setup>
const store = useStore()

callOnce(() => store.fetchUser(), { mode: 'navigation' })
</script>
<template>
<div v-if="store.loading">Loading...</div>
<div v-else>
<!-- User data display -->
</div>
</template>

<script setup>
const store = useStore()

callOnce(() => store.fetchUser(), { mode: 'navigation' })
</script>
This approach allows the navigation to proceed immediately while the data fetching happens in the background. The page will render with a loading state, and then update once the data is available. Additionally, you can use Nuxt's <NuxtLoadingIndicator> component to show a progress bar during navigation, which can improve the perceived performance:
<template>
<NuxtLoadingIndicator />
<!-- Rest of your app -->
</template>
<template>
<NuxtLoadingIndicator />
<!-- Rest of your app -->
</template>
This will add a progress bar between page navigations, providing visual feedback to the user while data is being fetched. By combining these techniques, you can avoid blocking the navigation while still ensuring that your data is fetched and your UI reflects the loading state appropriately. Source: Docs > Api > Utils > callOnce Source: Docs > Getting-started > Data Fetching > Suspense ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3d ago
<@210839019674796033> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
SaltTheWound
SaltTheWoundOP3d ago
@kapa.ai By removing the await i loose ssr.
kapa.ai
kapa.ai3d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- You're correct, and I apologize for the oversight in my previous response. Removing the await from callOnce would indeed cause you to lose server-side rendering (SSR) capabilities. This is an important point, and I'll explain why and suggest a better approach. The callOnce utility is designed to work with SSR, and awaiting it is necessary to ensure the data is fetched on the server before rendering. According to the Docs > Api > Utils > callOnce, callOnce is meant to be called directly in a setup function, plugin, or route middleware, because it needs to add data to the Nuxt payload to avoid re-calling the function on the client when the page hydrates. Given this information, here's a more appropriate solution to maintain SSR while avoiding navigation blocking: 1. Keep using await callOnce in your setup function to preserve SSR:
<script setup>
const store = useStore()
await callOnce(() => store.fetchUser(), { mode: 'navigation' })
</script>
<script setup>
const store = useStore()
await callOnce(() => store.fetchUser(), { mode: 'navigation' })
</script>
2. To prevent navigation blocking, you can use Nuxt's <Suspense> component in your layout or parent component. This allows the content to be rendered while async operations are pending:
<template>
<Suspense>
<template #default>
<!-- Your page content -->
<NuxtPage />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<template>
<Suspense>
<template #default>
<!-- Your page content -->
<NuxtPage />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
3. You can also use the <NuxtLoadingIndicator> component as mentioned before to show a progress bar during navigation:
<template>
<NuxtLoadingIndicator />
<Suspense>
<!-- ... -->
</Suspense>
</template>
<template>
<NuxtLoadingIndicator />
<Suspense>
<!-- ... -->
</Suspense>
</template>
This approach allows you to: - Maintain SSR by awaiting callOnce - Avoid blocking navigation by using <Suspense> - Provide a smooth user experience with loading indicators Remember, the navigation mode of callOnce will ensure that the function is called once during the initial render (either SSR or CSR) and once per subsequent client-side navigation. Source: Docs > Api > Utils > callOnce Source: Docs > Getting-started > Data Fetching > Suspense ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3d ago
<@210839019674796033> Kapa.ai is still learning and improving, please let me know how I did by reacting below.

Did you find this page helpful?