Improve User Experience with a Minimal Loading State: A Vue Composable for Making Delay Ref
Tags: vue
Published: 2023-02-26 03:55:26

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.

[What is a composable](https://vuejs.org/guide/reusability/composables.html#mouse-tracker-example)
> 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.

```js
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.
```js
const loading = ref(false)
const delayLoading = refMinDelay(loading, 500)
```


Create a delay loading state from a prop.
```js
const data = computed(() => props.data)
const delayLoading = refMinDelay(data, 500, (value) => value == null)
```


Create a delay state of the source.
```js
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](https://stackblitz.com/edit/vitejs-vite-a6ahfq?file=src/App.vue) 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`.

{% embed https://stackblitz.com/edit/vitejs-vite-a6ahfq?embed=1&file=src/App.vue %}


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.

![as description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/72u4xjti9fzfvhj4yy81.gif)


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.

![as description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2prukofcy0i2757v0ecr.gif)