Async state and network
Let useAsyncState() own the loading, error and race handling for any promise, wait for a ref with until(), and know which useFetch() you are calling.
What you'll learn
When you finish, you can do these.- 1Load on a click and show each state with useAsyncState()
- 2Keep only the latest answer when requests race
- 3Handle a failed load with error and onError
- 4Read JSON with VueUse's useFetch() and .json()
Picture a page that loads a short quote about a topic you type. The quote comes from a server, so the page has four states to show: not started, loading, ready and failed. By hand, that takes a value for the answer, one for loading and one for the error, plus a try/finally block, which runs its last step even when a request fails. Most hand-written versions also miss a race: two requests in flight, where the older answer lands last and overwrites the newer one.
VueUse, a library of ready-made functions for Vue, handles all of it with useAsyncState(), which wraps any function that returns a promise, JavaScript's object for an answer that arrives later. The Code tab holds the quote page built with it, App.vue, and its fake server, api.ts. You need one idea from Sensors and the viewport: a VueUse function hands you refs and keeps them up to date, as useElementSize() did with a note box's size. A ref is a box that holds a value and tells Vue when it changes.
One composable for loading, ready and failed
useAsyncState() is a composable, a function that hands you refs: here, refs that say which state the quote page is in.
In App.vue, a ref called topic holds the text of the topic box, Vue at first. In api.ts, loadQuote() plays the server: each answer takes 0.4 to 1.2 seconds, an empty topic fails, and the line Answers received counts the answers that come back. App.vue hands loadQuote() to useAsyncState():
const { state, isLoading, error, execute } = useAsyncState(
(t: string) => loadQuote(t),
'No quote yet',
{ immediate: false, resetOnExecute: false },
)
The first argument is the function to run: it takes the topic as t and passes it to loadQuote(). The second, No quote yet, is the starting value of state, the ref that holds the answer. The third holds options: immediate: false waits for a click instead of running straight away, and resetOnExecute: false keeps the old quote on screen while the next one loads, instead of going back to No quote yet.
The call also returns isLoading, which is true during a run, and error, which holds a failure. A fourth ref, isReady, says whether the latest run has its answer; App.vue does not use it.
execute() starts a run: a delay in milliseconds first, then the arguments for your function. In each run, await, JavaScript's keyword for waiting on a promise, pauses until loadQuote() answers or fails.
- execute(0, topic)
- isLoading true
- await loadQuote(topic)
- state or error
- isLoading false
In the template, the button calls execute(0, topic): no delay, and the topic from the box.
<button @click="execute(0, topic)">Load a quote</button>
<p v-if="isLoading">Loading…</p>
<p v-if="error">{{ error }}</p>
<blockquote v-else>{{ state }}</blockquote>
Click Load a quote: Loading… appears, then the quote "Quote 1: Vue gets easier one small step at a time." The v-else belongs to the v-if right before it, the error check, so the quote shows unless the run failed.
Only the latest answer counts
Quick clicks start several runs, their answers arrive in any order, and the newest run still wins.
Each call to execute() is numbered. When an answer arrives, useAsyncState() puts it in state only if it belongs to the most recent call, and isLoading turns false only when that call finishes. Older answers are dropped.
execute() also returns a promise of its own call's data: const quote = await execute(0, topic.value) works where you need the value in code. Copy it into your own ref and the race comes back: each call returns its own answer, stale or not. For display, read state.
Errors, until() and useFetch()
A failed run needs a message on the page, and VueUse has two more helpers: until() waits for a ref, and useFetch() loads a URL.
When a run fails, its error lands in error, and the template shows it in place of the quote. With an empty topic, the page reads "Error: Type a topic first." By default useAsyncState() also passes it to the browser's reportError(), which reports it as uncaught, as if nothing had handled it. Pass an onError function in the options to take that job over.
until() turns a ref into something you can await. For example, useOnline() gives a ref, isOnline, that is true while the browser has a connection. An async function can start with await until(isOnline).toBe(true) to wait until the connection comes back.
VueUse's useFetch(url) wraps fetch, the browser's own function for requesting a URL, and gives you data, error and isFetching. It reads the body, what the server sends back, as text. For JSON, text written like a JavaScript object, chain .json() onto the call to get an object. With refetch: true it fetches again when its URL is a ref that changes, cancelling the request in flight.
Nuxt, a framework built on Vue, has its own useFetch(), which runs on the server and sends the result to the browser inside the page. In a Nuxt project that is the one you get: the @vueuse/nuxt module leaves VueUse's out of its automatic imports.
What goes wrong
The first mistake is the loader that useAsyncState() replaces. Written by hand, App.vue would keep two refs and a load() function:
const quote = ref('No quote yet')
const loading = ref(false)
async function load() {
loading.value = true
quote.value = await loadQuote(topic.value)
loading.value = false
}
Load a quote about Vue, change the topic to Pinia and load again before the first answer arrives. When the Vue answer is slower, it lands last and the page ends on the old topic. Loading… also disappears too early, because the first answer to arrive sets loading to false. With useAsyncState(), only the latest call counts:
const { state, isLoading, execute } = useAsyncState(
(t: string) => loadQuote(t),
'No quote yet',
{ immediate: false },
)
The page ends on the Pinia quote, and Loading… stays until that call finishes.
The second mistake forgets how VueUse's useFetch() reads the body. The quote page has no real server, so picture a product card on a shop page. Written as a component, it is one file, ProductCard.vue, which is not in the Code tab. It asks your server for /api/product/1, which answers with JSON: {"name":"Mug","price":12}.
const { data } = useFetch('/api/product/1')
{{ data?.name }} renders nothing, and {{ data }} shows the whole answer as one string, because data holds the raw text. (?. skips .name while data is still null.)
const { data } = useFetch('/api/product/1').json()
With .json(), data holds an object, and {{ data?.name }} shows Mug.
What to remember
useAsyncState()gives youstate,isLoading,isReady,errorandexecute().immediate: falsewaits for a click, andresetOnExecute: falsekeeps the old answer while the next one loads.- Only the most recent
execute()call setsstate. The valueexecute()returns comes from its own call, stale or not. stateis a shallow ref: replace its value, or passshallow: falseto change data inside it.- Errors land in
errorand are also reported as uncaught, unless you passonError. - VueUse's
useFetch()returns text unless you chain.json(). In Nuxt,useFetch()means Nuxt's own.
Try it
Open the Code tab and change the starter code. Each task says what you should see.- Click Load a quote three times quickly. Answers received counts up to 3, but the quote jumps straight to the one numbered by your last click.
- Empty the topic box and click Load a quote. The page shows
Error: Type a topic first.and the preview also reports it in red as uncaught. AddonError: () => {}to the options and try again: the red report is gone, and the message stays on the page. - Change
resetOnExecute: falsetotrue. Load one quote, then another: while the second loads, the quote area shows No quote yet instead of the previous quote.