How can you implement a loading spinner following an HTTP request, or any asynchronous operation that occurs over time, using the specified logic?
Wait for X seconds (100ms) and display nothing.
If the data arrives within X seconds (100ms), display it immediately.
If the data does not arrive within X seconds (100ms), display the spinner for at least Y seconds (250ms) until the data arrives.
In Angular/RxJS, you would achieve this in your Service by:
/**
* We want to display the loading indicator only if the requests takes more than `${INITIAL_WAITING_TIME}
* if it does, we want to wait at least `${MINIMUM_TIME_TO_DISPLAY_LOADER} before emitting
*/
const startLoading$ = of({}).pipe(
tap(() => this.setState({ loading: true })),
delay(MINIMUM_TIME_TO_DISPLAY_LOADER),
switchMap(() => EMPTY)
);
const hideLoading$ = of(null).pipe(
tap(() => this.setState({ loading: false }))
);
const timer$ = timer(INITIAL_WAITING_TIME).pipe();
/**
* We want to race two streams:
*
* - initial waiting time: the time we want to hold on any UI updates
* to wait for the API to get back to us
*
* - data: the response from the API.
*
* Scenario A: API comes back before the initial waiting time
*
* We avoid displaying the loading spinner altogether, and instead we directly update
* the state with the new data.
*
* Scenario B: API doesn't come back before initial waiting time.
*
* We want to display the loading spinner, and to avoid awkward flash (for example the response comes back 10ms after the initial waiting time) we extend the delay to 250ms
* to give the user the time to understand the actions happening on the screen.
*/
const race$ = race(timer$, data$).pipe(
switchMap((winner) =>
typeof winner === 'number' ? startLoading$ : EMPTY
)
);
return concat(race$, hideLoading$, data$).pipe(filter(Boolean));
Alternatively, in Vue, you can use setTimeout
and nested watch
to handle changes to reactive properties like so:
<script setup lang="ts">
import { ref, watch } from 'vue'
const displayUI = ref<boolean>(false);
const loading = ref<boolean>(false);
const data = ref<string | null>(null);
setTimeout(() => {
// after 100ms we want to display a UI to the user
displayUI.value = true;
// if data has arrived, we can display and exit this logic
if (data.value) {
return;
}
// if it has not arrived
// we show spinner for at least 250ms
loading.value = true;
setTimeout(() => {
// at this point, we should display data, but only after data has arrived
// Question is: without RxJS, how can we
if (data.value) {
loading.value = false;
} else {
// can we nest a watcher?
watch(data, (value) => {
if (value) {
loading.value = false
data.value = 'it worked!'
}
})
}
}, 2500)
}, 1000)
// fake timer, let's say our API request takes X amount of time to come back
setTimeout(() => {
data.value = 'Data arrived'
}, 4000)
</script>
<template>
<template v-if="displayUI">
<h1 v-if="!data && !loading">
No Data
</h1>
<h1 v-if="!loading && data">
{{ data }}
</h1>
<h1 v-if="loading">
Loading...
</h1>
</template>
</template>