N
Nuxt3w ago
timmy

How can I share a custom data fetching composable between pages and components?

I have a composable for doing CRUD operations on data to my backend API. Something like:
const useAlbums = () => {
const {data, refresh, error, status} = useFetch("/api/albums", { key: "albums" })

const createAlbum = (values) => {
try {
const { data } = $fetch("/api/albums/create/", {
method: "POST",
body: values
});
refresh()
} catch {
throw createError(...)
}
}

return {
data,
createAlbum,
// ...
}
}
export default useAlbums;
const useAlbums = () => {
const {data, refresh, error, status} = useFetch("/api/albums", { key: "albums" })

const createAlbum = (values) => {
try {
const { data } = $fetch("/api/albums/create/", {
method: "POST",
body: values
});
refresh()
} catch {
throw createError(...)
}
}

return {
data,
createAlbum,
// ...
}
}
export default useAlbums;
I can call this from my pages:
<script setup lang="ts">
const { data: albums, createAlbum } = useAlbums();

const onFormSubmit = (values) => createAlbum(values)
</script>
<script setup lang="ts">
const { data: albums, createAlbum } = useAlbums();

const onFormSubmit = (values) => createAlbum(values)
</script>
This is a nice pattern, but if I use my composable in multiple pages/components it results in numerous calls to the API. I imagine this is expected behaviour: every time the composable is called, a new useFetch call is made etc. How can I make this composable (or data) "global" so that any page/component that loads the composable is accessing the same data, with out it having to re-fetch from the server (which only needs to happen on a) initial load and b) calling refresh) I imagine there are a number of possibilities: 1. Use Pinia 2. Make the composable global (not sure if possible) 3. Use some sort of caching with useFetch (not sure if possible) 4. Use useState (don't fully understand if this is a solution)
9 Replies
kapa.ai
kapa.ai3w ago
Hi @timmy. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- To share a custom data fetching composable between pages and components in Nuxt, while avoiding unnecessary API calls, you can use the useState composable along with useAsyncData. This approach allows you to create a reactive and SSR-friendly shared state across components. Here's how you can modify your useAlbums composable to achieve this:
import { useState } from '#app'

const useAlbums = () => {
const albums = useState('albums', () => null)

const { data, refresh, error, status } = useAsyncData(
'albums',
() => $fetch("/api/albums"),
{
lazy: true,
server: false,
transform: (result) => {
albums.value = result
return result
}
}
)

const createAlbum = async (values) => {
try {
await $fetch("/api/albums/create/", {
method: "POST",
body: values
})
refresh()
} catch (error) {
throw createError(...)
}
}

return {
albums,
createAlbum,
refresh,
error,
status
}
}

export default useAlbums
import { useState } from '#app'

const useAlbums = () => {
const albums = useState('albums', () => null)

const { data, refresh, error, status } = useAsyncData(
'albums',
() => $fetch("/api/albums"),
{
lazy: true,
server: false,
transform: (result) => {
albums.value = result
return result
}
}
)

const createAlbum = async (values) => {
try {
await $fetch("/api/albums/create/", {
method: "POST",
body: values
})
refresh()
} catch (error) {
throw createError(...)
}
}

return {
albums,
createAlbum,
refresh,
error,
status
}
}

export default useAlbums
This approach combines useState and useAsyncData to create a shared state for albums that can be accessed across different components without triggering multiple API calls. Here's a breakdown of the changes: 1. We use useState to create a shared reactive state for albums Docs > Getting-started > State Management (https://nuxt.com/docs/getting-started/state-management). 2. We use useAsyncData instead of useFetch to have more control over the data fetching process Docs > Api > Composables > useAsyncData (https://nuxt.com/docs/api/composables/use-async-data). 3. We set lazy: true to prevent automatic execution of the fetch on component mount. 4. We set server: false to only fetch on the client-side, avoiding unnecessary server-side fetches. 5. We use the transform option to update the shared state when data is fetched. Now, when you use this composable in your components or pages, it will share the same state:
<script setup lang="ts">
const { albums, createAlbum, refresh } = useAlbums()

// Fetch albums if not already loaded
if (!albums.value) {
refresh()
}

const onFormSubmit = (values) => createAlbum(values)
</script>
<script setup lang="ts">
const { albums, createAlbum, refresh } = useAlbums()

// Fetch albums if not already loaded
if (!albums.value) {
refresh()
}

const onFormSubmit = (values) => createAlbum(values)
</script>
This approach addresses your concerns: 1. It avoids using Pinia, which might be overkill for this use case. 2. It makes the composable "global" by using shared state. 3. It utilizes caching through useAsyncData and useState. 4. It leverages useState to create a shared reactive state. By using this pattern, you ensure that the data is fetched only once and shared across all components that use the useAlbums composable. The refresh function can be called whenever you need to update the data from the server. Docs > Getting-started > State Management (https://nuxt.com/docs/getting-started/state-management) provides more information on using useState for shared state management in Nuxt. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3w ago
<@487733342284939277> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
timmy
timmyOP3w ago
@kapa.ai this doesn't seem to work. The API call is still made every time the page loads
kapa.ai
kapa.ai3w ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- I apologize for the confusion in my previous answer. You're right that the approach I suggested doesn't fully solve the issue of multiple API calls. Let's revisit this problem and find a more effective solution. The reason why the API call is still being made every time the page loads is that the useAsyncData call is recreated each time the composable is used. To address this, we need to implement a more robust caching mechanism. Here's an improved approach using useAsyncData with proper caching:
import { useAsyncData, useState } from '#app'

const useAlbums = () => {
const albums = useState('albums', () => null)

const { data, refresh, error, status, execute } = useAsyncData(
'albums',
() => $fetch("/api/albums"),
{
server: true,
lazy: true,
transform: (result) => {
albums.value = result
return result
},
getCachedData: () => albums.value
}
)

const createAlbum = async (values) => {
try {
await $fetch("/api/albums/create/", {
method: "POST",
body: values
})
refresh()
} catch (error) {
throw createError(...)
}
}

if (!albums.value) {
execute()
}

return {
albums,
createAlbum,
refresh,
error,
status
}
}

export default useAlbums
import { useAsyncData, useState } from '#app'

const useAlbums = () => {
const albums = useState('albums', () => null)

const { data, refresh, error, status, execute } = useAsyncData(
'albums',
() => $fetch("/api/albums"),
{
server: true,
lazy: true,
transform: (result) => {
albums.value = result
return result
},
getCachedData: () => albums.value
}
)

const createAlbum = async (values) => {
try {
await $fetch("/api/albums/create/", {
method: "POST",
body: values
})
refresh()
} catch (error) {
throw createError(...)
}
}

if (!albums.value) {
execute()
}

return {
albums,
createAlbum,
refresh,
error,
status
}
}

export default useAlbums
This updated version makes use of several key features of useAsyncData to prevent unnecessary API calls: 1. We use getCachedData option to return the cached data if it exists Docs > Api > Composables > useAsyncData (https://nuxt.com/docs/api/composables/use-async-data#getcacheddata). This ensures that if the data is already in the state, it won't trigger a new fetch. 2. We set lazy: true to prevent automatic execution of the fetch when the composable is called. 3. We manually call execute() if albums.value is null, ensuring that the data is fetched only once when it's not available. 4. We keep server: true to allow server-side rendering if needed, but the caching mechanism will prevent duplicate fetches on the client side. Now, when you use this composable in your components or pages, it should only make the API call once, and subsequent uses will use the cached data:
<script setup lang="ts">
const { albums, createAlbum, refresh } = useAlbums()

// No need to manually call refresh here, the composable handles it
</script>
<script setup lang="ts">
const { albums, createAlbum, refresh } = useAlbums()

// No need to manually call refresh here, the composable handles it
</script>
This approach leverages Nuxt's built-in caching mechanisms for useAsyncData Docs > Getting-started > Data fetching (https://nuxt.com/docs/getting-started/data-fetching#keys). The getCachedData option ensures that the data is fetched only once and then reused across all components that use the useAlbums composable. If you still experience multiple API calls with this implementation, it might be worth checking if your pages or components are being unmounted and remounted, which could cause the composable to be re-initialized. In such cases, you might need to consider using a more global state management solution like Pinia or moving the state management to a plugin. I apologize again for the initial confusion, and I hope this revised solution better addresses your issue with preventing multiple API calls. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3w ago
<@487733342284939277> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
timmy
timmyOP3w ago
@kapa.ai yes, the composables are being re-initialised as I'm changing pages and reloading the composables.
kapa.ai
kapa.ai3w ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- I understand your concern. You're right that when changing pages in a Nuxt application, components and their associated composables are typically unmounted and remounted, which can lead to re-initialization of the composables and potentially trigger unnecessary API calls. To solve this issue, we need to implement a more persistent state management solution. Given your scenario, the best approach would be to use Pinia for global state management. Pinia is the officially recommended state management solution for Vue and Nuxt applications, and it's designed to work well with the Nuxt SSR environment. Here's how you can refactor your useAlbums composable to use Pinia: 1. First, install Pinia if you haven't already:
npm install @pinia/nuxt
npm install @pinia/nuxt
2. Add Pinia to your nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
})
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
})
3. Create a Pinia store for albums:
// stores/albums.ts
import { defineStore } from 'pinia'

export const useAlbumsStore = defineStore('albums', {
state: () => ({
albums: null,
error: null,
status: null
}),
actions: {
async fetchAlbums() {
this.status = 'loading'
try {
this.albums = await $fetch("/api/albums")
this.status = 'success'
} catch (error) {
this.error = error
this.status = 'error'
}
},
async createAlbum(values) {
try {
await $fetch("/api/albums/create/", {
method: "POST",
body: values
})
await this.fetchAlbums()
} catch (error) {
throw createError(...)
}
}
}
})
// stores/albums.ts
import { defineStore } from 'pinia'

export const useAlbumsStore = defineStore('albums', {
state: () => ({
albums: null,
error: null,
status: null
}),
actions: {
async fetchAlbums() {
this.status = 'loading'
try {
this.albums = await $fetch("/api/albums")
this.status = 'success'
} catch (error) {
this.error = error
this.status = 'error'
}
},
async createAlbum(values) {
try {
await $fetch("/api/albums/create/", {
method: "POST",
body: values
})
await this.fetchAlbums()
} catch (error) {
throw createError(...)
}
}
}
})
4. Update your useAlbums composable to use the Pinia store:
// composables/useAlbums.ts
import { storeToRefs } from 'pinia'
export const useAlbums = () => {
const store = useAlbumsStore()
const { albums, error, status } = storeToRefs(store)

if (!albums.value) {
store.fetchAlbums()
}

return {
albums,
createAlbum: store.createAlbum,
refresh: store.fetchAlbums,
error,
status
}
}
// composables/useAlbums.ts
import { storeToRefs } from 'pinia'
export const useAlbums = () => {
const store = useAlbumsStore()
const { albums, error, status } = storeToRefs(store)

if (!albums.value) {
store.fetchAlbums()
}

return {
albums,
createAlbum: store.createAlbum,
refresh: store.fetchAlbums,
error,
status
}
}
Now, when you use this composable in your components or pages, it will use the Pinia store, which persists across page navigations:
<script setup lang="ts">
const { albums, createAlbum } = useAlbums()

// Albums will be fetched only once, even across page navigations
</script>
<script setup lang="ts">
const { albums, createAlbum } = useAlbums()

// Albums will be fetched only once, even across page navigations
</script>
This approach using Pinia offers several advantages: 1. The state persists across page navigations, preventing unnecessary API calls. 2. It provides a centralized place for managing your application's state. 3. It works well with Nuxt's SSR capabilities. By using Pinia, you ensure that your album data is fetched only once and shared across all components and pages, even when navigating between different routes in your Nuxt application. For more information on using Pinia with Nuxt, you can refer to the Docs > Getting-started > State Management > Usage with Pinia (https://nuxt.com/docs/getting-started/state-management#usage-with-pinia) section in the Nuxt documentation. Stack Overflow Post > Vue Ref nolonger reactive after being returned from composable Part 3 (https://stackoverflow.com/questions/77395749) also discusses a similar issue and confirms that using Pinia is a good solution for sharing state between different components in a Nuxt application. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai3w ago
<@487733342284939277> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
NightFuries
NightFuries3w ago
Hi there. Lemme see if i fully understand your problem here. You want to fetch some data from a custom api and this should be available globally on every page? You'd want to set this data once and not have to call it on every individual page, am i right?
Want results from more Discord servers?
Add your server