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