The Game of Loaders
By – Karanbir Kajal (Engineer, Web)
This post is about improving the placeholders/loaders in UrbanClap Web. The overall client-side user experience today is majorly grouped into these 3 buckets:
- Continuity: This is defined by how fluent the current user journey is till the end of the funnel. A simple example can be a food ordering flow. The less the user finds abruptness in the flow from selecting a restaurant to the last payment step, the more continuous the whole flow is, and hence the user is more likely to convert. If, at any step, the user finds any sort of abruptness, he/she is likely to break from the funnel.
Continuity is a part of both the actual product, as well as the tech that is involved. - User interactivity: With micro interactions gaining popularity today, it is important that all aspects of user interaction are well covered in any app. The list ranges from simple aspects such as trivial user feedback on tap, to complex flows including multiple screens and popups. In every case, the user should not only be in full control of the flow, but also enjoy the whole journey as he/she goes along.
- Jankiness: All animations in any flow are expected to be smooth, with a high frame rate (60 frames per second ideally). A frame rate less than this leads to stuttering that a user experiences when there is motion on the screen. This choppiness is considered as jank. For a smooth animation, things like layout, paint, composition etc. have to be considered which are the building blocks of a frame.
This post will only touch the continuity aspect of client-side performance. Other topics will be covered in my subsequent posts.
Previous Architecture: Centralised and Stateless
Since we use React, there used to be a single, centralised loader component <Loader /> inside the root component. Every other component used to call/hide this loader directly by using these fixed utility functions (simplified for demonstration):
function showLoader() {
document.getElementById('loader').style.display = 'block';
}function hideLoader() {
document.getElementById('loader').style.display = 'none';
}
and it took the whole of client’s viewport width and height with the following CSS:
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
Whenever there was a need to display a loading state in the browser, any component used to directly call this loader and the whole screen used to get blocked because of its design. As a result, we faced 2 major issues in this approach:
Issue #1: It was impossible to localize this loader to a specific part of the screen. If a <section> wanted to show a loader that was constrained only within its boundaries, it could not, as shown below.
Issue #2: There were conflicts between asynchronous events to hide/show this loader. As an example, consider a button that triggers 3 promises in parallel. Each of these 3 promises make a call to showLoader() in pending state and call hideLoader() when resolved or rejected. In this case, even if one promise gets resolved, the loader gets hidden, even if the other 2 promises are still in pending state.
Because of the above issues, the user experience was compromised, hence there was a need for a better loader handling design.
Current Architecture: Distributed and Stateful
- Since there is a need to have local loaders in a containers, spread only in specific parts of the screen, we need to have multiple loaders. React makes it easy for us since we can just plug and play the loader components at the root of any other component that needs it.
- Rather than a component having direct control over a loader, there is a need for a stateful logic to determine when to actually hide a loader, even when a components demands so.
Each parent component now has its own loader component with a unique id passed via props.
<Loader refId="testLoader1" />
<Loader refId="testLoader2" />
<Loader refId="testLoader3" />
The utility functions to show and hide loaders are also updated based on loader ID.
function showLoader(id) {
document.getElementById(id).style.display ='block';
}function hideLoader(id) {
document.getElementById(id).style.display = 'none';
}
But now, components DO NOT call these utility functions directly. Instead, we maintain a separate store (redux) for loaders. Every component now dispatches an action to this loader store and do not have direct control over the loader component.
As an example, consider 3 sections, each having their own loaders with ids as testLoader1, testLoader2 and testLoader3. We first maintain an initial loader state in our loader store as:
const initialState = {
testLoader1: 0,
testLoader2: 0,
testLoader3: 0
}
Then we define the reducer actions, callLoader and dismissLoader, that the components will call instead of the utility functions defined before.
function callLoader(loaderId, boolReset) {
return {
type: INCREMENT_LOADER_COUNT,
loaderId,
boolReset
};
}function dismissLoader(loaderId, boolReset) {
return {
type: DECREMENT_LOADER_COUNT,
loaderId,
boolReset
};
}
Finally, we create the reducer function for the same with the following logic:
- A loader will be visible only if its corresponding count in the loader store is greater than zero
- A loader will be hidden only if its corresponding count in the loader store is exactly zero
function loaderData(state = initialState, action = {}) {
let updatedCount = state[action.loaderId];
switch (action.type) {
case INCREMENT_LOADER_COUNT:
updatedCount = updatedCount + 1;
if (updatedCount > 0) {
showLoader(action.loaderId);
}
return {
...state,
[action.loaderId]: updatedCount
};
case DECREMENT_LOADER_COUNT:
updatedCount = action.boolReset ? 0 : updatedCount - 1;
if (updatedCount === 0) {
hideLoader(action.loaderId);
}
return {
...state,
[action.loaderId]: updatedCount
};
default:
return state;
}
}
An extra feature of resetting the count may also come in handy.
Components call theses actions which then alter the corresponding loader counters. It is only when the count of a particular loader reaches 0, that loader hidden from the DOM.
The new design handles both the localization, and conflicting issues easily.
About the author: Karanbir Kajal is part of the engineering team, working on our core Web architecture. He has been a strong house, having worked across platform and product flows, as well as across the stack.
Sounds like fun?
If you enjoyed this blog post, please clap 👏(as many times as you like) and follow us (UrbanClap Blogger) . Help us build a community by sharing on your favourite social networks (Twitter, LinkedIn, Facebook, etc).
You can read up more about us on our publications —
https://medium.com/urbanclap-design
https://medium.com/urbanclap-engineering
If you are interested in finding out about opportunities, visit us at http://careers.urbanclap.com