runWithContext not working as expected

I have following async composable:
import type { CraftSite } from 'vue-craftcms'
type SeoData = {
MetaTitleContainer: { title?: { title: string } }
}

export async function useCraftSeoMatic(uri?: string | Ref<string>, site?: CraftSite | Ref<CraftSite>) {
const { baseUrl, siteMap } = useRuntimeConfig().public.craftcms
const currentUri = uri ? ref(uri) : useCraftUri()
const apiEndpoint = computed(() =>
`${baseUrl}/actions/seomatic/meta-container/all-meta-containers/?asArray=true&uri=${encodeURIComponent(currentUri.value)}`,
)

const { data: seoMaticData, error } = await useAsyncData(
'seoMaticData',
async () => {
const response = await $fetch<SeoData>(apiEndpoint.value)
if (!response || typeof response !== 'object') {
console.error('Transformation of SEOmatic data failed, please verify that the SEOmatic endpoint is working correctly')
return undefined
}
return {
title: response.MetaTitleContainer?.title?.title ?? '',
};
}
)

if(error.value) {
console.error(error.value)
}

const app = useNuxtApp()

app.runWithContext(() => {
useHead({
title: seoMaticData.value?.title,
})
})
}
import type { CraftSite } from 'vue-craftcms'
type SeoData = {
MetaTitleContainer: { title?: { title: string } }
}

export async function useCraftSeoMatic(uri?: string | Ref<string>, site?: CraftSite | Ref<CraftSite>) {
const { baseUrl, siteMap } = useRuntimeConfig().public.craftcms
const currentUri = uri ? ref(uri) : useCraftUri()
const apiEndpoint = computed(() =>
`${baseUrl}/actions/seomatic/meta-container/all-meta-containers/?asArray=true&uri=${encodeURIComponent(currentUri.value)}`,
)

const { data: seoMaticData, error } = await useAsyncData(
'seoMaticData',
async () => {
const response = await $fetch<SeoData>(apiEndpoint.value)
if (!response || typeof response !== 'object') {
console.error('Transformation of SEOmatic data failed, please verify that the SEOmatic endpoint is working correctly')
return undefined
}
return {
title: response.MetaTitleContainer?.title?.title ?? '',
};
}
)

if(error.value) {
console.error(error.value)
}

const app = useNuxtApp()

app.runWithContext(() => {
useHead({
title: seoMaticData.value?.title,
})
})
}
and i use it like that in the app.vue
<script setup lang="ts">
// Fetch & apply seomatic properties
await useCraftSeoMatic()
</script>
<script setup lang="ts">
// Fetch & apply seomatic properties
await useCraftSeoMatic()
</script>
Why do I still get that error A composable that requires access to the Nuxt instance
8 Replies
kapa.ai
kapa.ai3mo ago
Hi @Samuelreichoer. 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 explain why you're still encountering the "A composable that requires access to the Nuxt instance" error despite using runWithContext. The issue stems from how async composables work in Nuxt and the limitations of the Nuxt context. Here's why you're facing this problem: 1. Async Composables: As explained in the Vue and Nuxt Composables documentation, you cannot use await before calling a composable, except in specific cases like <script setup>, defineNuxtComponent, defineNuxtPlugin, or defineNuxtRouteMiddleware. Your useCraftSeoMatic function is an async composable, which can lead to context issues. 2. Context Loss: After an await statement, the Nuxt context is typically lost. This is because Nuxt (and Vue) unset the global instance immediately after each tick to avoid leaking shared references between users or components, especially important for server-side rendering. 3. runWithContext Usage: While runWithContext is designed to restore context, it's not a complete solution for all async scenarios. As mentioned in the runWithContext documentation, this method should be used sparingly and isn't always a guaranteed fix. 4. Nested Async Calls: Your composable contains nested async calls (useAsyncData inside useCraftSeoMatic), which can complicate context management. To address this issue, you have a few options: 1. Move the useHead call outside of the composable: Instead of calling useHead inside useCraftSeoMatic, return the SEO data and call useHead in your <script setup> where the context is properly maintained. 2. Use defineNuxtPlugin: If you need to keep this logic centralized, consider moving it into a Nuxt plugin where the context is more reliably maintained. 3. Experimental asyncContext: Nuxt has an experimental feature called asyncContext that might help. You can enable it in your nuxt.config.ts:
export default defineNuxtConfig({
experimental: {
asyncContext: true
}
})

export default defineNuxtConfig({
experimental: {
asyncContext: true
}
})

This feature is described in the Native Async Context PR, but keep in mind it's experimental and may not be suitable for production use yet. Remember, as stated in the Nuxt App documentation, the Nuxt context is only reliably accessible in specific places like plugins, Nuxt hooks, Nuxt middleware, and setup functions. Structuring your code to work within these constraints is often the most reliable approach. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3mo ago
<@1065266379382587422> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Samuelreichoer
SamuelreichoerOP3mo ago
@kapa.ai Alright so I use this useFetch and want to return all of these properties without loosing reacitvity const { data: seoMaticData, error } = await useFetch(apiEndpoint, { transform: (seoMaticData: SeoData) => { if (!seoMaticData || typeof seoMaticData !== 'object') { console.error('Transformation of SEOmatic data failed, please verify that the SEOmatic endpoint is working correctly') return undefined } return { title: seoMaticData.MetaTitleContainer?.title?.title ?? '', metaTags: generateMetaTags(seoMaticData.MetaTagContainer), linkTags: generateLinkTags(seoMaticData.MetaLinkContainer), jsonLd: seoMaticData.MetaJsonLdContainer ?? {}, }; }, }) how do I do this?
kapa.ai
kapa.ai3mo ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- To return all the properties from your useFetch call without losing reactivity, you can use the approach described in the Nuxt documentation. Here's how you can modify your code to achieve this:
const { data: seoMaticData, error, refresh } = await useFetch(apiEndpoint, {
transform: (seoMaticData: SeoData) => {
if (!seoMaticData || typeof seoMaticData !== 'object') {
console.error('Transformation of SEOmatic data failed, please verify that the SEOmatic endpoint is working correctly')
return undefined
}
return {
title: seoMaticData.MetaTitleContainer?.title?.title ?? '',
metaTags: generateMetaTags(seoMaticData.MetaTagContainer),
linkTags: generateLinkTags(seoMaticData.MetaLinkContainer),
jsonLd: seoMaticData.MetaJsonLdContainer ?? {},
};
},
})
const { data: seoMaticData, error, refresh } = await useFetch(apiEndpoint, {
transform: (seoMaticData: SeoData) => {
if (!seoMaticData || typeof seoMaticData !== 'object') {
console.error('Transformation of SEOmatic data failed, please verify that the SEOmatic endpoint is working correctly')
return undefined
}
return {
title: seoMaticData.MetaTitleContainer?.title?.title ?? '',
metaTags: generateMetaTags(seoMaticData.MetaTagContainer),
linkTags: generateLinkTags(seoMaticData.MetaLinkContainer),
jsonLd: seoMaticData.MetaJsonLdContainer ?? {},
};
},
})
This approach maintains reactivity for all the properties returned by the transform function. Here's why this works: 1. The useFetch composable returns reactive refs for data, error, and other properties. These refs will update automatically when the data changes or is refreshed. 2. The transform function is applied to the fetched data before it's stored in the data ref. This means that seoMaticData will be a reactive ref containing the transformed object. 3. You can access the properties of the transformed data reactively using seoMaticData.value.title, seoMaticData.value.metaTags, etc. 4. If you need to refresh the data, you can use the refresh function returned by useFetch. To use this data in your component or composable, you can do:
const title = computed(() => seoMaticData.value?.title)
const metaTags = computed(() => seoMaticData.value?.metaTags)
const linkTags = computed(() => seoMaticData.value?.linkTags)
const jsonLd = computed(() => seoMaticData.value?.jsonLd)
const title = computed(() => seoMaticData.value?.title)
const metaTags = computed(() => seoMaticData.value?.metaTags)
const linkTags = computed(() => seoMaticData.value?.linkTags)
const jsonLd = computed(() => seoMaticData.value?.jsonLd)
This approach ensures that all properties remain reactive and will update if the underlying data changes. For more information on how useFetch works with reactivity, you can refer to the Nuxt Data Fetching documentation. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3mo ago
<@1065266379382587422> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Samuelreichoer
SamuelreichoerOP3mo ago
When I return it like that return { title: computed(() => seoMaticData.value?.title), metaTags: computed(() => seoMaticData.value?.metaTags), linkTags: computed(() => seoMaticData.value?.linkTags), jsonLd: computed(() => seoMaticData.value?.jsonLd), } and import it like that const { metaTags, title, linkTags, jsonLd} = await useCraftSeoMatic() I get an ts error that Vue: Property metaTags does not exist on type { title: ComputedRef<string | undefined>; metaTags: ComputedRef<Record<string, any>[] | undefined>; linkTags: ComputedRef<Record<string, any>[] | undefined>; jsonLd: ComputedRef<...>; } | undefined @kapa.ai When I return it like that return { title: computed(() => seoMaticData.value?.title), metaTags: computed(() => seoMaticData.value?.metaTags), linkTags: computed(() => seoMaticData.value?.linkTags), jsonLd: computed(() => seoMaticData.value?.jsonLd), } and import it like that const { metaTags, title, linkTags, jsonLd} = await useCraftSeoMatic() I get an ts error that Vue: Property metaTags does not exist on type { title: ComputedRef<string | undefined>; metaTags: ComputedRef<Record<string, any>[] | undefined>; linkTags: ComputedRef<Record<string, any>[] | undefined>; jsonLd: ComputedRef<...>; } | undefined
kapa.ai
kapa.ai3mo ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- The TypeScript error you're encountering is due to the way you're destructuring the result of an async function. The issue arises because TypeScript can't guarantee that the useCraftSeoMatic function will always return a non-undefined value, hence the union type with undefined. To resolve this issue, you have a few options: 1. Use optional chaining when destructuring:
const { metaTags, title, linkTags, jsonLd } = (await useCraftSeoMatic()) ?? {};
const { metaTags, title, linkTags, jsonLd } = (await useCraftSeoMatic()) ?? {};
2. Assert the type if you're sure it won't be undefined:
const { metaTags, title, linkTags, jsonLd } = await useCraftSeoMatic() as {
title: ComputedRef<string | undefined>;
metaTags: ComputedRef<Record<string, any>[] | undefined>;
linkTags: ComputedRef<Record<string, any>[] | undefined>;
jsonLd: ComputedRef<Record<string, any> | undefined>;
};
const { metaTags, title, linkTags, jsonLd } = await useCraftSeoMatic() as {
title: ComputedRef<string | undefined>;
metaTags: ComputedRef<Record<string, any>[] | undefined>;
linkTags: ComputedRef<Record<string, any>[] | undefined>;
jsonLd: ComputedRef<Record<string, any> | undefined>;
};
3. Use a separate variable and check for undefined:
const result = await useCraftSeoMatic();
if (result) {
const { metaTags, title, linkTags, jsonLd } = result;
// Use the variables here
}
const result = await useCraftSeoMatic();
if (result) {
const { metaTags, title, linkTags, jsonLd } = result;
// Use the variables here
}
4. Modify your useCraftSeoMatic function to always return a non-undefined value:
export async function useCraftSeoMatic(uri?: string | Ref<string>, site?: CraftSite | Ref<CraftSite>) {
// ... your existing code ...

return {
title: computed(() => seoMaticData.value?.title ?? ''),
metaTags: computed(() => seoMaticData.value?.metaTags ?? []),
linkTags: computed(() => seoMaticData.value?.linkTags ?? []),
jsonLd: computed(() => seoMaticData.value?.jsonLd ?? {}),
}
}
export async function useCraftSeoMatic(uri?: string | Ref<string>, site?: CraftSite | Ref<CraftSite>) {
// ... your existing code ...

return {
title: computed(() => seoMaticData.value?.title ?? ''),
metaTags: computed(() => seoMaticData.value?.metaTags ?? []),
linkTags: computed(() => seoMaticData.value?.linkTags ?? []),
jsonLd: computed(() => seoMaticData.value?.jsonLd ?? {}),
}
}
This last approach ensures that even if seoMaticData.value is undefined, you'll always return an object with the expected properties, avoiding the union with undefined. Remember, when working with async composables in Nuxt, you need to be careful about maintaining reactivity and context. As mentioned in the Vue and Nuxt Composables documentation, using await before calling a composable can lead to context issues. Make sure you're using useCraftSeoMatic in a place where async usage is supported, such as within a <script setup>, defineNuxtComponent, or defineNuxtPlugin. Source If you continue to face issues, you might want to consider restructuring your composable to avoid the async nature, or use one of Nuxt's built-in async-safe composables like useLazyAsyncData or useLazyFetch. Source ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3mo ago
<@1065266379382587422> Kapa.ai is still learning and improving, please let me know how I did by reacting below.

Did you find this page helpful?