· Front-end Technology · 4 min read
Use useAsyncState to Fetch Async State in Just One Line
Introduction
This article demonstrates a state management approach that achieves asynchronous data retrieval with reactivity in just one line of code. It also includes corresponding state and a promise that can be awaited.
<script setup lang="ts">
const userStore = useUserStore()
const loaded = ref(false)
const userProfileResult = userStore.getUserProfile('00001')
</script>
<template>
<div v-if="userProfileResult.isReady">
<div>Name: {{ userProfileResult.state.name }}</div>
<div>Gender: {{ userProfileResult.state.gender }}</div>
</div>
</template>
Version 1
Do you handle data like this?
<script setup lang="ts">
const userProfile = ref<UserProfile | null>(null);
const loaded = ref(false)
async function loadData() {
const response = await fetch('https://api.example.com/user');
userProfile.value = await response.json();
loaded.value = true
}
onMounted(() => {
loadData()
});
</script>
Version 2
This approach certainly works. If multiple components use the same data, you might consider using a Pinia store.
export const useUserStore = defineStore('user', () => {
const userProfile = ref<UserProfile | null>(null);
const loaded = ref(false)
async function loadData() {
const response = await fetch('https://api.example.com/user');
userProfile.value = await response.json();
loaded.value = true
}
return {
userProfile,
loaded,
loadData
}
})
Version 3
It seems fine with a single global user, usually the logged-in user.
However, one day the page requirements became more complex. In addition to displaying the current user’s information, you also need to display other users’ information. At this point, you might use computed
.
export const useUserStore = defineStore('user', () => {
const userProfileArray = ref<UserProfile[]>([]);
async function loadData(userId: string) {
const response = await fetch('https://api.example.com/user/' + userId);
userProfileArray.value.push(await response.json());
}
function getUserProfile(userId: string) {
return computed(() => userProfileArray.value.find(user => user.id === userId))
}
return {
userProfile,
loadData
}
})
In the page:
<script setup lang="ts">
const userStore = useUserStore()
const loaded = ref(false)
const userProfile = userStore.getUserProfile('00001')
onMounted(async () => {
await userStore.loadData('00001')
loaded.value = true
});
</script>
<template>
<div v-if="loaded">
<div>Name: {{ userProfile.name }}</div>
<div>Gender: {{ userProfile.gender }}</div>
</div>
</template>
In this way, different pages can load the same or different userId
s, and the retrieved data remains reactive. Additionally, updates in one component will reflect in other components.
Version 4
However, you also noticed a shortcoming: loadData
and getUserProfile
are quite separate. Is there a way to combine them?
import { useAsyncState } from '@vueuse/core'
export const useUserStore = defineStore('user', () => {
const userProfileMap = reactive<Map<string, UserProfile>>(new Map());
const getUserProfileGetter = (userId: string) : () => {
state: UserProfile | undefined
isReady: Ref<boolean>
isLoading: Ref<boolean>
execute: (...args: any) => Promise<any>
} => {
const userProfile = userProfileMap.get(userId)
if (!userProfile) {
const { state: fetchedUserProfiles, isReady, isLoading, execute } = useAsyncState(fetch('https://api.example.com/user/' + userId), null, {
onSuccess: (fetchedUserProfiles) => {
fetchedUserProfiles.forEach(userProfile => userProfileMap.set(userProfile.id, userProfile))
},
})
return () => {
return {
state: userProfileMap.get(userId),
isReady,
isLoading,
execute,
}
}
}
return () => {
return {
state: userProfileMap.get(userId),
isReady: ref(true),
isLoading: ref(false),
execute: () => Promise.resolve(),
}
}
}
const getUserProfile = (userId: string) => {
return computed(getUserProfileGetter(userId))
}
return {
getUserProfile,
}
})
This version has a lot of updates. We’ll discuss them one by one later. For now, let’s see how to use it.
<script setup lang="ts">
const userStore = useUserStore()
const loaded = ref(false)
const userProfileResult = userStore.getUserProfile('00001')
</script>
<template>
<div v-if="userProfileResult.isReady">
<div>Name: {{ userProfileResult.state.name }}</div>
<div>Gender: {{ userProfileResult.state.gender }}</div>
</div>
</template>
With this, our Vue pages become much simpler. The usage is more straightforward, standardized, and elegant.
Detailed Explanation
- Since we aim to provide a global service, we naturally need to ensure that the same
id
globally points to the same value. Therefore, we converted theuserProfileArray
array to:
const userProfileMap = reactive<Map<string, UserProfile>>(new Map());
- To have a globally reactive value, we use
computed
:
const getUserProfile = (userId: string) => {
return computed(() => userProfileMap.get(userId))
}
- When the data in memory doesn’t exist, we need to fetch it asynchronously:
const getUserProfile = (userId: string) => {
return computed(() => {
const userProfile = userProfileMap.get(userId)
if (!userProfile) {
fetch('https://api.example.com/user/' + userId).then(response => {
userProfileMap.set(userId, response.json())
})
}
return userProfile
})
}
This approach works, but initially, the data obtained externally is empty and there’s no loading state. Therefore, we utilize useAsyncState.
Next, we separate the getter part into a standalone function to enable reuse in
update
:
const updateUserProfile = async (user: Partial<UserProfile> & { id: string }) => {
const userGetter = getUserProfileGetter(user.id)
const existingUser = userGetter()
await existingUser.execute()
if (existingUser) {
const updatedUser = { ...existingUser.state, ...user }
userProfileMap.set(user.id, updatedUser)
await fetch('https://api.example.com/user/update/', {
method: 'PUT',
body: JSON.stringify(updatedUser),
})
} else {
await fetch('https://api.example.com/user/update/', {
method: 'PUT',
body: JSON.stringify(user),
})
getUserProfileGetter(user.id)
}
}
- Note the use of
execute
. Withoutexecute
, there’s no corresponding asynchronous execution to wait for the result, and polling cannot be used. Although the demo doesn’t explicitly demonstrate this, by reviewing type declarations and source code, this is identified as a good path.
Conclusion
With this, we have achieved asynchronous retrieval of reactive data in Vue components with just one line of code. It also includes corresponding state and a promise that can be awaited.
This article was written in a hurry. If there are any mistakes, please feel free to point them out.