docs
  1. Developer Guide
  2. Technical Foundation
  3. Data & State

Data & State

Developing a modern e-commerce application often requires fetching significant amounts of data for a single page, such as category details, product information, and potentially enriching this data by fetching from external sources. Efficiently managing these asynchronous operations and handling the resulting state across your application is crucial for performance and maintainability.

The SCAYLE Storefront Application, built on Nuxt 3, leverages Nuxt's powerful built-in composables like useAsyncData to address these challenges. useAsyncData is the recommended way to fetch asynchronous data in Nuxt 3, handling server-side rendering (SSR), client-side navigation, caching, error handling, and data hydration automatically.

Asynchronous Data Fetching with useAsyncData

The useAsyncData composable takes an asynchronous handler function where you place your data fetching logic. This handler function is executed on the server during SSR and on the client during navigation. Within this handler, you have full control over how multiple fetch calls are executed: serially, in parallel, or a combination.

const { data, pending, error, refresh } = useAsyncData(
  'unique-fetch-key', // Required key for caching/hydration
  async () => {
    // Your data fetching logic goes here
    // e.g., calling RPC methods, fetching from other APIs
    return resultOfTheFetch
  }
)

For Storefront Composables, the key is typically auto-generated internally. You might need to pass it manually, for instance, to simplify debugging of _asyncData or when using useNuxtData.

Serial Execution within useAsyncData

Place your fetch calls using await inside the useAsyncData handler to execute them sequentially. This is necessary when a subsequent fetch call depends on the result of a previous one.

const { data, pending, error } = useAsyncData('serialFetchExample', async () => {
  const resultA = await fetchA() // Assume fetchA() returns a Promise
  // fetchB depends on resultA
  const resultB = await fetchB(resultA) // Assume fetchB() returns a Promise
  return { resultA, resultB } // Return the combined data
});

While this approach ensures the correct order of execution, it can potentially increase overall page loading times due to the sequential nature of the requests.

Parallel Execution within useAsyncData

If your data fetch calls are independent, use Promise.all within the useAsyncData handler to execute them in parallel. This can significantly reduce the total loading time.

const { data, pending, error } = useAsyncData('parallelFetchExample', async () => {
  const [resultA, resultB, resultC] = await Promise.all([
    fetchA(), // Assume fetchA() returns a Promise
    fetchB(), // Assume fetchB() returns a Promise
    fetchC()  // Assume fetchC() returns a Promise
  ])
  return { resultA, resultB, resultC } // Return the combined data
})

This pattern initiates all fetch calls simultaneously and waits for all of them to complete before returning the final data.

Mixed Execution within useAsyncData

You can combine serial and parallel execution patterns within the useAsyncData handler. For example, you might start some independent fetches while waiting for others that have dependencies.

const { data, pending, error } = useAsyncData('mixedFetchExample', async () => {
  // Start fetchC immediately - this runs in parallel with the A/B sequence
  const promiseC = fetchC() // Assume fetchC() returns a Promise

  // Execute fetchA and fetchB serially
  const resultA = await fetchA() // Assume fetchA() returns a Promise
  const resultB = await fetchB(resultA) // Assume fetchB() returns a Promise

  // Wait for the parallel fetchC to complete
  const resultC = await promiseC

  // Return the combined results
  return { resultA, resultB, resultC }
});

This allows for fine-grained control over your data fetching strategy, balancing dependencies with performance within a single useAsyncData call.

Shared State and Caching with the useAsyncData Key

The useRpc composable, used by many Storefront composables, internally also relies on shared state behavior that can be manually disabled:

One of the most powerful features of useAsyncData is its built-in state management and caching based on the unique key provided as its first argument. This key is used by Nuxt's internal state tree, allowing the state (data, pending, error) associated with a specific key to be shared across your application.

This key-based sharing is crucial for SSR and hydration. Data fetched on the server using useAsyncData with a specific key is automatically serialized and sent to the client. When useAsyncData is called on the client with the same key, Nuxt rehydrates the state from the server payload instead of refetching the data, ensuring a smooth transition and immediate reactivity.

SCAYLE Storefront composables, including those that wrap RPC methods, are designed to leverage useAsyncData and this key-based sharing mechanism.

Consider a useNews composable (which internally uses useAsyncData):

// In ComponentA
const newsA = useNews('my-news-section') // Calls useAsyncData('my-news-section', ...)

// In ComponentB
const newsB = useNews('my-news-section') // Calls useAsyncData('my-news-section', ...)

// Let's assume ComponentA or a parent component triggers the fetch
// e.g., await newsA.fetch() if useNews provides a fetch method wrapping useAsyncData's execute/refresh,
// or Nuxt automatically fetches on SSR/navigation.

// After the fetch associated with 'my-news-section' key completes:
console.log("NewsA data", newsA.data.value) // Will output data from the fetch
console.log("NewsB data", newsB.data.value) // Will output the *same* data as newsA.data.value
console.log(newsA.data.value === newsB.data.value) // => true

In this scenario, both newsA and newsB instances returned by useNews point to the same reactive data state managed by useAsyncData under the 'my-news-section' key. This is a fundamental pattern: you can call the same composable in multiple components, but the underlying data fetching and state management for a given key happen only once per request/navigation, and all instances using that key access the shared state.

Preventing State Sharing

If you require independent state for different instances of the same composable or for data that should not be shared via the key, provide unique keys or omit the key (though omitting the key disables caching and is generally not recommended for fetched data). Removing the key will prevent sharing data and trigger independent calls for the usage of a useAsyncData instance without request deduping. As Storefront Composables automatically generate unique key internally, hydration will still work as expected between server and client.

// In ComponentA
const newsA = useNews('news-section-A') // Calls useAsyncData('news-section-A', ...)

// In ComponentB
const newsB = useNews('news-section-B') // Calls useAsyncData('news-section-B', ...)

// After fetch associated with 'news-section-A' completes:
console.log("NewsA data", newsA.data.value) // Will output data from its fetch

// Data for 'news-section-B' hasn't been fetched yet (unless triggered elsewhere)
console.log("NewsB data", newsB.data.value) // Will likely be null or initial state

console.log(newsA.data.value === newsB.data.value) // => false (unless both are null/undefined)

By providing distinct keys, useAsyncData treats them as separate data sources, managing their state, caching, and hydration independently.

Understanding how useAsyncData's key facilitates shared state and integrates with Nuxt's SSR and hydration is crucial for effectively handling data and building performant components in the SCAYLE Storefront Application.

More details can be found in the official Nuxt "Data Fetching" Guide or the "useAsyncData vs. useFetch" YouTube video by Alexander Lichter, a Core Maintainer of Nuxt.