Data loading
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:
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
defineDmsPageMetawaits for the middleware it starts, so what a middleware loads, or sets withtransfer: 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,messageanddata; any other failure becomes a500whose 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: falseandimmediate: falseread 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), oruseDmsFetchwith a body it cannot key, such as aFormData) cannot be transferred: the server leaves it to the browser, asserver: falsedoes. A development render warns about it, and about a backend read made outside any handler on the server, such as a setup's ownawait $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.
errorstays 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: msreads 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, and0,nullorundefinedstops 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 member | Behaviour |
|---|---|
await or not | The result's refs are there at once; awaiting it waits for the data |
lazy: true | Spares client navigations the wait; a server render still waits |
server: false | The server renders the default; the browser fetches once the page is mounted |
immediate: false | Waits 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 |
deep | data 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, placeholder | The key the data is loaded under; placeholder is true while a reactive key shows the previous key's data |
refreshOnPageRefresh: false | Opts the call out of refreshDmsPageData() |
A null or undefined key | Nothing 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 thedataanother caller loads underkey, as Nuxt'suseNuxtData. Assigning it, or changing it in place when the key isdeep: 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 forexecute()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.