Fetching resources and displaying a loading UI is a common task when building Vue applications. One simple approach is to use v-if="data" and v-else to toggle the UI, but this can become problematic if the APIs you're working with respond too quickly. In such cases, the loading UI may flash briefly and distract users, creating a poor user experience.
While working recently, I encountered a situation where the APIs I was using became extremely fast after a colleague moved them to AWS. As a result, the flashing of the loading UI became too noticeable and disruptive for users. To solve this problem, I created a composable.
In the context of Vue applications, a "composable" is a function that leverages Vue's Composition API to encapsulate and reuse stateful logic.
My implementation
My initial plan was to create a ref exclusively for tracking the loading state. However, I thought that it would be more versatile if we could manipulate its value instead of merely using a boolean.
export function refMinDelay(
sourceRef,
delay,
conditionToDoSomethingImmediate = newValue => newValue === true,
doImmediate = newValue => true,
doAfterDelay = newValue => false,
) {
let timeId = 123
const delayRef = ref(sourceRef.value)
watch(sourceRef, () => {
if (conditionToDoSomethingImmediate(sourceRef.value) === true) {
clearTimeout(timeId)
delayRef.value = doImmediate(sourceRef.value)
timeId = setTimeout(() => {
timeId = null
if (conditionToDoSomethingImmediate(sourceRef.value) === true)
return
delayRef.value = doAfterDelay(sourceRef.value)
}, delay)
}
else {
if (timeId)
return
delayRef.value = doAfterDelay(sourceRef.value)
}
}, {
immediate: true,
})
return delayRef
}
The refMinDelay function takes in a sourceRef, a delay time in milliseconds, and optional parameters conditionToDoSomethingImmediate, doImmediate, and doAfterDelay. It returns a reactive delayRef that is updated based on the sourceRef updates and the specified delay.
If the sourceRef updates and the conditionToDoSomethingImmediate function evaluates to true, the delayRef will be immediately updated to the return value of the doImmediate function. If conditionToDoSomethingImmediate evaluates to false, the delayRef will only update to the return value of the doAfterDelay function if the specified delay has passed. if the delay has not yet passed, the update will be deferred until the minimal delay is up.
If sourceRef is a boolean loading state, the default values for conditionToDoSomethingImmediate, doImmediate, and doAfterDelay are provided to create a delay loading state. For example, refMinDelay(loading, 500) will create a delay loading state with a 500ms delay.
When to use
When fetching data and displaying a loading UI, frequent switches between the loading UI and the data display can cause the screen to flash, which can disrupt the user's experience. To create a smoother transition, a minimal loading state can be used to delay the switch from the loading UI to the data display until a minimal delay has passed, ensuring that the loading state is visible for at least that duration.
How to use
Create a delay loading state from an existing loading state.
const loading = ref(false)
const delayLoading = refMinDelay(loading, 500)
Create a delay loading state from a prop.
const data = computed(() => props.data)
const delayLoading = refMinDelay(data, 500, (value) => value == null)
Create a delay state of the source.
const source = ref({ id: 1})
const delay = refMinDelay(
source,
500,
(v) => v == null, /* condition to set value immediately */
(v) => v, /* set value immediately */
(v) => v /* set value once minimal delay is up*/
);
Example
You can play the example on stackblitz or use the embedded version below. Click the +1 button at different speeds and adjust the delay values for both delayLoading and responseTime to see different results.
To learn how refMinDelay works, right-click an element and select "inspect", go to the console tab, and check what's logged from refMinDelay.js.
If the API is very fast, the loading UI will still be shown until the minimum delay time has elapsed. For example, responseTime takes 100ms and delayLoading is set to 300ms.
If the API takes longer than the delay to respond, there will be no additional delay. For example, responseTime takes 500ms and delayLoading is set to 300ms.


Top comments (1)
Great write up! Was planning to implement something similar to this today and just randomly stumbled across this post