[data-reveal]{opacity:1!important;transform:none!important}
Data loading
Frontend

Data loading

useDmsAsyncData, useDmsFetch, useDmsPagedData and useDmsState, how a server render waits for and transfers their data, and how the browser revalidates it.

Use useDmsAsyncData when a component needs data from a route. It lets the server wait for the result and pass it to the browser, so hydration does not repeat the request. Components using the same key share the result.

For example, in a Vue component's <script setup>, read an existing /api/orders route:

components/Orders.vue — script setup
const { data, error, status, refresh } = useDmsAsyncData(
  "orders",
  ({ signal }) => $fetch("/api/orders", { signal }),
);

data.value holds the route's response; error.value holds a failure. Call refresh() after an action that changes the orders. The endpoint is a prerequisite, not a route this composable creates, and the key must include any filters that distinguish one result from another.

useDmsFetch(url, options) is useDmsAsyncData around a request, and useDmsPagedData reads a list page by page. In the DMS, useApiData and useApiPagedData wrap them; see the frontend composables of the DMS guide.

If you know Nuxt 4's useAsyncData, the main differences are that calls with one key are joined, a failed refresh keeps the last successful data, and the first lazy run starts after the caller's turn. The reference below covers server rendering, caching and revalidation; you do not need to configure those for the first request.

On the server

  • A server render waits for the data. A component's data is awaited before the component renders, whether or not its setup awaits the call, and the browser takes the result over while it hydrates instead of fetching it again. Keys a setup does not await load in parallel. Data a plugin starts is awaited before the page renders.
  • A page renders once its middleware is done. The page that calls defineDmsPageMeta waits for the middleware it starts, so what a middleware loads, or sets with transfer: true, is what the HTML shows and the browser hydrates with. A middleware's redirect still answers the request. A middleware that throws fails the page: the error page shows in its place, and a server render answers with the error's status code. Middlewares run in order; those registered with { parallel: true } next to each other start together, their results applying in registration order.
  • The browser hydrates with what the HTML shows. Data that settles once the HTML is complete is left to the browser. A value a setup derives from data it did not await, in an immediate watcher for instance, is then the one the server rendered.
  • A failure is transferred too, so hydration does not run the handler again. An HTTP failure keeps its statusCode, statusMessage, message and data; any other failure becomes a 500 whose cause only the server logs. On a server, a fetch error's message names the backend's address, so its status text replaces it.
  • A new key is read where the first one would have been. On a server, server: false and immediate: false read nothing. Another read follows a key its setup sets: the component's render waits for it and transfers it. A key that changes once that wait has started is not read on the server; the HTML shows the default, and the browser reads it once the component is mounted. To render a read that depends on another one's answer, await the first one in the setup.
  • A call without a key (useDmsAsyncData(handler), or useDmsFetch with a body it cannot key, such as a FormData) cannot be transferred: the server leaves it to the browser, as server: false does. A development render warns about it, and about a backend read made outside any handler on the server, such as a setup's own await $fetch, which the browser makes again.

The payload

One <script id="dms-payload" type="application/json"> carries the data, the failures and the transferred state, in devalue's format, which keeps a Set, a Map or a Date, escaped like Inertia's page object. A value it cannot write, such as a class instance, is left out and logged.

useDmsState(key, init, { transfer: true }) hands the server's value to the browser, which then starts from it instead of calling init. State is never transferred unless a key opts in, and must never hold a token or a secret: the value is written into the page.

In the browser

  • Stale while revalidate. Data loaded before shows at once, and is read again in the background on each mount of a component showing it and on each return of a reactive key to it; awaiting the call does not wait for that read. The first mount after a server render does not read again. Of the data no component shows any more, the browser keeps the 50 most recently shown across navigations, so a return to a page shows its data at once.
  • error stays until the next answer. A run in flight keeps the last failure; a success clears it, a failure replaces it, and a failed refresh keeps the data of the last success on screen.
  • refreshInterval: ms reads again on an interval while a component shows the data and the page is visible. The interval may be a ref or a getter: the polling follows it, and 0, null or undefined stops it.
  • A call outside a component (a plugin, also past its first await, a callback) belongs to the application: its watchers and its polling run for as long as the application does.

Options and results

Option or memberBehaviour
await or notThe result's refs are there at once; awaiting it waits for the data
lazy: trueSpares client navigations the wait; a server render still waits
server: falseThe server renders the default; the browser fetches once the page is mounted
immediate: falseWaits for execute(), for a dialog or a drawer
dedupe"cancel" (default) aborts the run in flight when a newer one replaces it; "defer" waits for it
Handler's { signal }Aborted when a newer run replaces this one, when clear() resets the data, or when the last component showing the key unmounts
deepdata is a shallow ref by default, as in Nuxt 4: replace the value, or pass deep: true to track changes inside it
transform(value, { key })Receives the key; useDmsFetch's transform(value, { key, url }) also the URL the response answers
key, placeholderThe key the data is loaded under; placeholder is true while a reactive key shows the previous key's data
refreshOnPageRefresh: falseOpts the call out of refreshDmsPageData()
A null or undefined keyNothing to load: the caller holds its default and runs nothing until the key has a value

Requests

useDmsFetch(url, options) is keyed on its method, base URL, URL, query and body, not its headers: the callers of one request share it, and a server render transfers it. The URL, method, query, params and body may each be a ref or a getter, read again for each request, as in Nuxt's useFetch. A null or undefined URL requests nothing until it has a value. key names a key instead, and may be a ref or a getter too; $fetch replaces the fetch function, which receives the signal.

dmsRequestKey(url, { method, baseURL, query, params, body }) is the key useDmsFetch gives a request, without the scope: for refreshDmsData(keys), and for a test that looks for a response's entry.

Lists, page by page

useDmsPagedData(key, handler, { nextCursor, initialCursor }) reads a list page by page under one key, for "load more" and infinite lists. The handler receives { cursor, index, signal } and returns a page; nextCursor(page, pages) gives the next page's cursor, or null at the end.

The first page loads like any data: a server render waits for it and transfers it. loadMore() appends the next one, pages, hasMore and loadingMore follow it, and a failure lands in error with the pages kept. A revalidation reads every page loaded so far again and replaces them once all are in. Put what filters the list in the key: a new key starts again from its first page.

Updating and refreshing

  • useDmsData(key) gives the data another caller loads under key, as Nuxt's useNuxtData. Assigning it, or changing it in place when the key is deep: true, updates every component showing the key without reading it again. Keys are shallow by default, so replace the value unless the reader opts into deep reactivity. This applies a write's answer, an optimistic update or a realtime event. It loads nothing and keeps nothing alive; a write to a key nobody loaded is ignored, and a run in flight still writes its answer over an update, so a writer refreshes the key once the write is done.
  • refreshDmsPageData() reads again the data the components on screen show, after a save or on a realtime event. Data waiting for execute() is left alone.
  • refreshDmsData(keys?) reads given keys again.
  • setDmsAsyncDataScope(scope) puts the keys in a scope, such as a digest of the account and the workspace: the same key in another scope is another entry, and moving to another scope drops the data of every key. Set it on both sides before the page renders, with the same value, from a plugin registered with { enforce: "pre" }, before another module's plugin reads data.