⏳ Creating a Reusable Loading Screen Manager for Async Tasks in Typescript
It is often the case that the user of an application must wait while a request to a back-end is being processed. Usually, a loader is shown during that time to tell the user that stuff is happening. That’s good UX. When implementing a loader, I found myself repeating code that: shows a loader, awaits task completion, then closes the loader. So why not create a helper function that enhances this common pattern?
In this example, there is a loading controller service that shows a popup of a loader.
Since this is an async
function, each line with the await
keyword will await its completion before continuing the execution of the function.
This code does 4 things:
- creates a loader and awaits its instantiation
- awaits the loader’s presentation for the user to see
- awaits the request to the backend to be completed
- awaits the dismissal of the loader
However, there are some problems with this approach:
- we have to repeat this code for each back-end request the user makes
- does not handle success nor error response
- not generic nor robust
- boring
So let’s create a generic solution that we can use throughout our application with the example of a user login system! 🔑
1️⃣ Let’s say we have a login form with this function for signing in our user.
But this approach is not reusable. What if we wanted to reuse this load task for when a user creates an account?
So let’s create a reusable function for our load task.
Upon invoking the provisionLoadTask
function, the task
callback is the function that is invoked after presenting the loader. Once task
is resolved, the loader is dismissed.
Note that we can wait for multiple tasks to complete in parallel by calling Promise.all
:
provisionLoadTask(() => Promise.all([task1(), task2()]));
Or in order using an async
function:
provisionLoadTask(async () => {
const res = await task1();
return await task2(res);
});
We are now able to reuse this load task logic but there are some obvious problems with this solution:
- If the email and password are correct, how do we handle the resolution response?
- If the email and password are not correct, how do we handle the rejection error?
2️⃣ To address this, let’s add callbacks for success handling and error handling.
This is quite an improvement!
With this approach, notice how we can handle a login success and login failure with independent callbacks! Note these two callbacks will never both be executed.
It is important to note the use of isSuccessful
.
If await task()
is successful, isSuccessful
is set to true and the success
callback is called.
If await task()
is unsuccessful, isSuccessful
is set to false and then the error
callback is called.
This solution is missing a key feature though.
3️⃣ Let’s say that upon login success you want to route the user to a secured portal page like so:
If you run this code, you’ll notice that when you submit the login form, the loader will popup, do authentication stuff, dismiss, then you will route to the portal page. This is because the dismissal of the loader comes before the success
and error
callbacks. We want to dismiss the loader when we have completed all the tasks, including routing, or else it’s bad UX.
We can make an improvement by calling dismiss
after the success
and error
callbacks and add await
in front of callbacks so that the loader will not be dismissed until after all the awaited tasks in either callback are resolved and the callback returns! 🤓
And of course, there might be some code to execute on success or failure so let’s add a complete
callback also.
If you run this code, the loader will be dismissed after routing to the portal page.
There you have it! A generic solution for provisioning a loader while executing async tasks with proper error handling!