Curious about how Nuxt's useAsyncData and useFetch can operate both synchronously and asynchronously? Discover the intriguing mechanism behind this dual functionality in our latest blog post. Learn how to create functions that adapt to await usage, enhancing your understanding of Nuxt and empowering you to apply these patterns in your own projects. Check out a complete working demo and step-by-step code explanations to master this concept!
Get notified when we release new tutorials, lessons, and other expert Nuxt content.
You may have noticed something weird with useAsyncData
and useFetch
in Nuxt.
It’s possible to use them either synchronously, or asynchronously, with the await
keyword:
// Synchronously
const { data } = useFetch('some/api');
// Asynchronously
const { data } = await useFetch('some/api');
How is it able to do both?
I wondered this, so I took some time to investigate and figure it out.
In this article, I’ll show you how to make a function that can either be awaited (ie can run sync or async based on the it’s use with the await
keyword). In doing so, you’ll get a better understanding of how useAsyncData
works and could even apply the pattern yourself in your own code (if you want).
You can see a complete working demo here. Here’s the important part that we’ll end up writing:
function asyncOrSync() {
const asyncOperation = new Promise((resolve) => {
setTimeout(() => {
resolve({
data: "world",
});
}, 1000);
});
const enhancedPromise = Object.assign(asyncOperation, {
data: "hello",
});
return enhancedPromise;
}
Now, let me take a few moments to explain what’s actually going on here!
Let me start by putting together some plain old Javascript boilerplate for a demo. We’ll use this code to help us see how this all works:
import "./styles.css";
function writeLine(text) {
document.body.appendChild(document.createTextNode(text));
document.body.appendChild(document.createElement("br"));
}
async function main() {
}
main();
First, let’s build an async function. Once we get that working, we’ll see how we can make it work synchronously as well:
function asyncOrSync() {
// Create a promise for the async operation
const asyncOperationPromise = new Promise(
(resolve) => {
setTimeout(() => {
resolve({
data: "world",
});
}, 1000);
}
);
return asyncOperationPromise;
}
The asyncOrSync
function creates a Promise
and returns it. That’s the main point. The Promise
happens to wait a whole second and then resolve with an object that has a data
property.
This return value is like what useFetch
would return (I did that on purpose, of course).
We’ll update the main
function to see this function in action:
async function main() {
const asyncResult = await asyncOrSync();
writeLine(`Async result: ${asyncResult.data}`);
}
This will wait a full second, then write out “Async result: world”.
So far, nothing too interesting. Just a regular async function.
The next part is where it gets interesting.
Our next step is to add in the synchronous behaviour, so that this function behaves in both ways simultaneously. Sort of like a Schrodinger’s Function — how you actually use it determines whether it’s async or sync.
Here’s the trick: a Promise
is an object, and we can add properties to objects.
If we use Object.assign
we can add the properties from one object to another, and this is how we’re able to get both async and sync behaviour. Let’s update our asyncOrSync
function to see how we do this:
function asyncOrSync() {
// Create a promise for the async operation
const asyncOperationPromise = new Promise(
(resolve) => {
setTimeout(() => {
resolve({
data: "world",
});
}, 1000);
}
);
// Enhance the promise with initial state properties
const enhancedPromise = Object.assign(asyncOperationPromise, {
data: "hello",
});
return enhancedPromise;
}
The part we added in is this:
// Enhance the promise with initial state properties
const enhancedPromise = Object.assign(asyncOperationPromise, {
data: "hello",
});
We create a new Promise
, enhancedPromise
, by using Object.assign
to add on a new property from the object we pass in. In this case, we’re passing a different value, “hello”.
Now we can update the main
function to see both uses of the asyncOrSync
function in action at the same time:
async function main() {
const syncResult = asyncOrSync();
writeLine(`Sync result: ${syncResult.data}`);
const asyncResult = await asyncOrSync();
writeLine(`Async result: ${asyncResult.data}`);
}
When we run this, we’ll see “hello” printed to the screen immediately. One second later, we’ll then see “world” printed out. We’ve achieved our goal of async and sync in the same function!
We can make it even more clear what’s happening by renaming the values of the properties. Remember, we’re returning two distinct objects here — one for the async response inside the Promise
, and another returned immediately on the Promise
for the sync response.
Let’s update the property names to see this more clearly:
function asyncOrSync() {
// Create a promise for the async operation
const asyncOperationPromise = new Promise((resolve) => {
setTimeout(() => {
resolve({
// 👇 Update this to `async`
async: "world",
});
}, 1000);
});
// Enhance the promise with initial state properties
const enhancedPromise = Object.assign(asyncOperationPromise, {
// 👇 Update this to `sync`
sync: "hello",
});
return enhancedPromise;
}
We’ll also need to update our main
function accordingly:
async function main() {
const syncResult = asyncOrSync();
writeLine(`Sync result: ${syncResult.sync}`);
const asyncResult = await asyncOrSync();
writeLine(`Async result: ${asyncResult.async}`);
}
Because these are separate objects being returned, they don’t need to have the same shape at all. We can use completely different structures and names if we wanted to — though I’m not sure why we would.
In Nuxt, the data fetching composables return identical objects in both sync and async mode for the convenience. No matter how you need to use the composable it will return the same thing, the only difference is whether or not you’ve put an await
in there.
We’re effectively returning a T | Promise<T>
type from this function. Either we can use the object itself, or the object inside of a Promise
.
Of course, we don’t actually need this type of response in most of our Javascript code. The only reason this is actually useful to us is because of Vue’s reactivity.
If we’re not using reactivity, there’s really no point to this whole pattern.
But with reactivity, we can receive the empty reactive containers from the composable, hook everything up with our computed
, watch
and other reactive logic, and then, when those values finally
resolve, they are reactively updated.
Or, we could simply await
for those values to resolve before moving on.
At least now it’s your choice.
If you want to check out the actual source code of useAsyncData
yourself, you can find that here. And I highly encourage it! You’ll probably learn some other interesting things about how Nuxt works.