How to handle loading progress flicker
Tags: javascript, typescript, webdev, ux
Published: 2021-12-29 07:37:48

One of the most important things in our web apps UX is to provide a form of feedback when the app needs more time to process the user's action when e.g an ajax request is fired in the background. This is usually done using a progress bar, a spinner, or a combination of both. 

Have you ever noticed though a "flaky" behavior when a lot of API calls happen at the same time?

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jx933xp84hvv8ekgu9y0.gif)

**We can provide a smooth experience to our users, so let's fix that!**

## What we'll build
We'll create a simple loader handler function for `Promises` that will make sure our loading indicator will disappear only when the last unresolved `Promise` gets resolved. We'll also make sure that the function is **reusable** so that it can be used with any project and with any progress loading indicator plugin.

## Implementation
`createLoader.ts`
```ts
export const createLoader = (options: {
  start: () => void;
  end: () => void;
  inc?: () => void;
  slackTime?: number;
}) => {
  const promises: Promise<unknown>[] = [];

  const opts = {
    ...{
      inc: () => {},
      slackTime: 0,
    },
    ...options,
  };

  const load = <T>(promise: Promise<T> | (() => Promise<T>)) => {
    const p = typeof promise === 'function' ? promise() : promise;

    !promises.length && opts.start();
    promises.push(p);

    p.finally(() => {
      setTimeout(() => {
        promises.pop();
        !promises.length ? opts.end() : opts.inc();
      }, opts.slackTime);
    });

    return p;
  };

  return { load };
};
```

We created a factory function that takes an options object as a parameter. The options object defines which operations will be performed on loading start/end and optionally an operation to execute when a `Promise` is resolved.

We can also define a `slackTime` that will be used as a delay to wait until the last `Promise` is resolved. This is useful in cases that we have e.g an API call that starts shortly after another call has just finished, causing a "flickering" effect in our loader.

The factory function will return our `load` method which will accept a `Promise` or a function that returns a `Promise`. Instead of firing our `async` functions directly, we'll pass them through our `load` function instead. Our loader then will make sure that the loading progress ends when the last of the `Promises` is resolved.

## Options
Option  | Description | Type | Default
--------|-------------|------|--------
`start` | A function to execute on loading start, this is where we start our progress loader  | `function` | `undefined`
`end` | A function to execute when loading ends, this is where we stop our progress loader | `function` | `undefined`
`inc` | An optional function to execute when a promise resolves (valid for all promises except the last one). We can use this function to increment our loading progress.  | `function` | `() => {}`
`slackTime` | Time in `ms` to wait until last promise is resolved as to enable multiple operations in a sequence without re-triggering a loader progress start | `Number` | `0`

## Creating our loader
It's time to create our loader. Here we're using the popular [nprogress](https://github.com/rstacruz/nprogress) library for our progress loading indicator as an example.

`loader.ts`
```ts
import NProgress from 'nprogress';
import { createLoader } from './createLoader';

NProgress.configure({
  minimum: 0.35,
  speed: 300,
});

const start = () => {
  NProgress.start();
};

const end = () => {
  NProgress.done();
};

const inc = () => {
  NProgress.inc(0.05);
};

const loader = createLoader({ start, end, slackTime: 350, inc });

export const { load } = loader;
```

We have configured the `NProgress` instance and setup a new loader that will start, end, or increment the `nprogress` progress bar depending on the `Promises`' state.

## How to use
Using our loader is fairly simple, we just need to pass our `Promises` into the `load` function and our loader will take care of the rest. Here's an example using `axios` that dispatches two requests so that they get registered in our loader.

```ts
import { load } from './loader';
import axios from 'axios';

const res1 = load(axios.get(`https://jsonplaceholder.typicode.com/todos`));
const res2 = load(axios.get(`https://jsonplaceholder.typicode.com/todos/1`));
```

**That's it**, no more loading progress flicker!

If you want to see how this feels in a browser, checkout a live example of the loader that implements a global page progress bar (and some extra stuff too) in this stackblitz [here](https://stackblitz.com/edit/vitejs-vite-yv9gg4?)

Make sure you open this with a Chromium-based browser (e.g Chrome) and run `npm run dev` on the console.

**Update:**
[promise-loading-handler](https://www.npmjs.com/package/promise-loading-handler) is now an npm package. You can include it in your JS project with `npm install promise-loading-handler`

Thanks for reading.