N
Nuxt3w ago
dmarr

Breaking app when awaiting composable in page setup

I have a pretty barebones use case that seems like it shouldn't be breaking things. On my page setup, I do the following:
<script setup lang="ts">
await useTest2();
</script>
<script setup lang="ts">
await useTest2();
</script>
In my composables:
export const useTest = () => useState<any>('test', () => null);
export const useTest2 = async () => {
await useFetch('https://api.github.com/users/octocat').then((data) => {
useTest().value = data.data;
});
}
export const useTest = () => useState<any>('test', () => null);
export const useTest2 = async () => {
await useFetch('https://api.github.com/users/octocat').then((data) => {
useTest().value = data.data;
});
}
This causes the dreaded A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function. error. I tried some of the workarounds, but was not successful. What am I missing here?
4 Replies
kapa.ai
kapa.ai3w ago
Hi @dmarr. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- The issue you're encountering is related to how Nuxt handles asynchronous operations in the setup function and composables. Let's break down the problem and provide a solution based on the knowledge sources. The error message you're seeing is a common one in Nuxt 3 when dealing with asynchronous operations and composables. The root cause is that when you await a composable directly in the setup function, it can lead to losing the Nuxt context, which is essential for composables to work correctly. Here's how we can fix this issue: 1. Instead of awaiting the composable directly, use the useAsyncData composable to handle the asynchronous operation. 2. Move the useFetch call directly into the setup function or into the useAsyncData handler. Here's the corrected code for your page:
<script setup lang="ts">
const { data } = await useAsyncData('test', () =>
useFetch('<https://api.github.com/users/octocat'>)
);

const test = useTest();
test.value = data.value;
</script>
<script setup lang="ts">
const { data } = await useAsyncData('test', () =>
useFetch('<https://api.github.com/users/octocat'>)
);

const test = useTest();
test.value = data.value;
</script>
And your composables can be simplified to:
export const useTest = () => useState<any>('test', () => null);
export const useTest = () => useState<any>('test', () => null);
This approach ensures that the Nuxt context is maintained throughout the asynchronous operation. The reason for this behavior is explained in the Nuxt 3 documentation on auto-imports:
During a component lifecycle, Vue tracks the temporary instance of the current component (and similarly, Nuxt tracks a temporary instance of nuxtApp) via a global variable, and then unsets it in same tick. This is essential when server rendering, both to avoid cross-request state pollution (leaking a shared reference between two users) and to avoid leakage between different components.
By using useAsyncData, we ensure that the asynchronous operation is handled correctly within the Nuxt lifecycle. If you need to use the useTest2 composable in multiple places, you can refactor it to use useAsyncData internally:
export const useTest2 = () => {
return useAsyncData('test', () =>
useFetch('<https://api.github.com/users/octocat'>)
);
}
export const useTest2 = () => {
return useAsyncData('test', () =>
useFetch('<https://api.github.com/users/octocat'>)
);
}
Then in your component, you can use it like this:
<script setup lang="ts">
const { data } = await useTest2();
const test = useTest();
test.value = data.value;
</script>
<script setup lang="ts">
const { data } = await useTest2();
const test = useTest();
test.value = data.value;
</script>
This approach maintains the composable pattern while ensuring proper context handling in Nuxt 3. Nuxt 3 Documentation: Auto-imports provides more details on this behavior and best practices for using composables in Nuxt 3. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3w ago
<@139883350096740354> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Cue
Cue3w ago
@dmarr The warning in the docs stipulates: “do not await useFetch in a composable, as that can cause unexpected behavior” The issue arising here is that you’re wrapping useFetch in a promise, and awaiting that promise, when it is expected that the promise returned from useFetch is to be resolved in setup/plugins/middleware. In short, do not await useFetch or useAsyncData anywhere besides the permitted locations.
dmarr
dmarrOP3w ago
ok.. thanks I feel like for the amount of time I use nuxt, (all day, every day haha) this should have been more obvious to me. I haven't seen that part of the docs, and I scour the useFetch/custom fetch wrapper stuff pretty consistently. Maybe the error message lacks some information that could make troubleshooting easier. The page it links to currently links to a verbose github issue with details that are no longer relevant. Ie, before certain fixes were added you had to do certain things to workaround the issue. The reason it cropped up for me was due to the reasons you said, but those weren't shown in the error... why wouldn't this work in a composable?
const { session } = useAuthSession();
const headers = useRequestHeaders(['cookie']);
session.value = await $fetch<AuthSession>('/api/auth/session', { headers });
const { session } = useAuthSession();
const headers = useRequestHeaders(['cookie']);
session.value = await $fetch<AuthSession>('/api/auth/session', { headers });
I'm not using useFetch Also this doesn't break, until I add the second useAsyncData..
const { data } = await useAsyncData('xyz', () => requestFetch('/api/echo'));
// const { data: foo } = await useAsyncData('foo', () => requestFetch('/api/echo'));
const { data } = await useAsyncData('xyz', () => requestFetch('/api/echo'));
// const { data: foo } = await useAsyncData('foo', () => requestFetch('/api/echo'));
I guess this works
const req1 = useAsyncData('xyz', () => requestFetch('/api/echo'));
const req2 = useAsyncData('foo', () => requestFetch('/api/echo'));
const [data1, data2] = await Promise.all([req1, req2]);
const req1 = useAsyncData('xyz', () => requestFetch('/api/echo'));
const req2 = useAsyncData('foo', () => requestFetch('/api/echo'));
const [data1, data2] = await Promise.all([req1, req2]);
I have literally hit every wall on the way to productivity haha

Did you find this page helpful?