Managing Complex Waiting Experiences on Web UIs
Waiting is not a concept we like. We are usually stressed at every point where “waiting” touches our lives. And on the internet, “waiting”…
Waiting is not a concept we like. We are usually stressed at every point where “waiting” touches our lives. And on the internet, “waiting” can be much more complicated than we thought it would be.
But apparently there is a long time we will have to “wait” until the tech guys solve the “speed” problem. ¯\_(ツ)_/¯
“You cannot manage what you cannot define.”
Part 1: Let’s define “waiting”!
On the Web, we frequently face the expression “loading”. I experienced that managing the “loading” experience is much more complicated than I thought, and I really had to manage it.
So, let’s define “waiting”:
In interfaces, “waiting” splits into 4 concepts which you can combine:
- Scope: Where the “waiting” happens.
- Specificity: If the “waiting” time is known.
- Interaction: If the “waiting” blocks the user or not.
- Reality: If the “waiting” is real or fake.
1. Scope
Defines where the “waiting” action happens. It may be two areas:
- 📦 Application [A]
Whole application will be loaded or some loading will affect whole application. E.g. Booting an operating system, opening a big software. - ✂️ Component [C]
A part of an application will be loaded. E.g. Loading timeline tweets while scrolling Twitter’s feed.
2. Specificity
Defines if we know when the “waiting” will end.
- ⏭ Determinate [D]
The waiting action has a specified ending. E.g. Downloading a file, watching a movie, listening to a music, etc. - ⏩ Indeterminate [I]
The waiting action does not have an ending. E.g. Streaming a live TV channel, listening to a radio, waiting for someone to write a reply to your comment, sending a request to a server, etc.
3. Interaction
Defines if the “waiting” action will block the interaction origin (user).
- 🔒 Blocking [B]
The waiting action is critical and the user shouldn’t touch specified area while something is happening. E.g. blocking user not to click “create” button again while creating something on back-end. - 🔓 Non-blocking [N]
The waiting action is not really critical and user can touch anywhere while something is happening. E.g. Allowing user to step into a specified time while YouTube movie is loading.
4. Reality
Defines if the “waiting” action is real or not real.
- 📝 Real [R]
The waiting action really waits for something to end in the background. E.g. User uploads their avatar to the server. - 💭 Fake [F]
The waiting action is not really waiting but it “feels” like doing something. It’s generally for UX purposes. E.g. Wait for server to create seed data.
“Waiting” Examples
Every combination of these 4 concepts has a value on UIs.
I don’t think to add Reality concept to the combination since it doesn’t make any difference on UI aspect.
Application, Determinate, Blocking 📦 ⏭ 🔒
Use cases: Splash screens.
Think of an application scope with blocking user and has a predictable ending. It’s basically a splash screen.
Splash screens are something which puts users to on hold and it doesn’t allow you to use anything while things completely set up.
In the example above, there is a progress bar that indicates the status of the waiting progress.
Info: If the “waiting” work has a predictable or specified ending (determinate), generally “progress bars” are being used.
Application, Determinate, Non-blocking 📦 ⏭ 🔓
Use cases: Top loader, global activity indicator.
Blocking user is not friendly for your application and when the waiting is on application scope, top loaders or global activity indicators are your friend.
Note: Predictable waitings should also be considered as “determinate” even they are “indeterminate”. A page loads in maximum 1–2 seconds. Thus it’s a good UX to use a progress bar.
Application, Indeterminate, Blocking 📦 ⏩ 🔒
Use cases: Splash screens (for mobile)
Waiting for an “indeterminate” action is generally shown as “activity indicators” which are circle loaders.
Application, Indeterminate, Non-blocking 📦 ⏩ 🔓
Use cases: Activity indicator, hourglass.
When you do not want to block user while something is happening on application, an activity indicator shown on screen which is generally minimal.
Component, Determinate, Non-blocking ✂️ ⏭ 🔓
Use cases: Background loaders, percentage texts.
While you actually wait for something to load, the UI will allow you to interact with it. YouTube’s “gray loader” actually covers this case.
Component, Indeterminate, Blocking ✂️ ⏩ 🔒
Use cases: Ghost loaders
Ghost loaders are most popular loaders nowadays. Building user interface with faking its content makes the layout more stable in UX aspect.
Combinations without examples, not very common:
- ✂️ ⏭ 🔒 Component, Determinate, Blocking: Download progress bars of browsers may be examples for that.
- ✂️ ⏩ 🔓 Component, Indeterminate, Non-blocking: Optimistic rendering with an activity indicator may be example for that.
Let’s summarize the combinations with a table:
Part 2: Let’s manage them!
After defining the concepts, managing them become easier.
Let’s assume we have an interface as below:
There are few simple steps to easily manage them:
1. Name every “wait”:
Naming waits is the first step you need to make. It allows you to trace and debug in complex states.
2. Collect them into a list:
The “waiting” names should be collected into an array:
Loaders should be in a global singleton loaders array. Also the names must be unique.let waitingFor = [
"fetching sidebar",
"fetching user",
"fetching content",
"sending content",
];
3. Add and remove them:
A simple push
to the waitingFor
before the actual work starts:// Start a new loader
waitingFor.push("logging in");await fetch("/login");
And to remove, filter
the waitingFor
and replace it with the new state.// End the loader
await fetch("/login");waitingFor = waitingFor.filter(l => l != "logging in");
4. Get the state of loaders:// Are there any loaders on application scope?
return waitingFor.length > 0;// Are there any specified loader?
return waitingFor.includes("logging in");
5. Collect progresses in a key-value store:
Keep progress information in another global singleton object.let progresses = {};
6. Starting a progress:
Also the waitingFor
should be appended.// Set the progress of "uploading avatar"waitingFor.push("uploading avatar");
progresses["uploading avatar"] = { current: 249, total: 1828 };
7. Ending a progress:waitingFor = waitingFor.filter(l => l != "uploading avatar");
const { ["uploading avatar"]: omit, ...progresses } = progresses;// or
delete progresses["uploading avatar"];
A Vue.js Implementation: vue-wait
According the ideas above, I created a tool called “vue-wait” that manages your loaders with a declarative API and useful Vue component.
vue-wait helps to manage multiple loading states on the page without any conflict. It’s based on a very simple idea that manages an array (or Vuex store optionally) with multiple loading states. The built-in loader component listens its registered loader and immediately become loading state.
Vue-wait is an implementation for Vue to manage loaders easily. It has a very simple API.
vue-wait provides a very simple API to start.<v-wait for='fetching data'>
<template slot='waiting'>
This will be shown when "fetching data" loader starts.
</template>
This will be shown when "fetching data" loader ends.
</v-wait>
And you can just start
and end
loaders.$wait.start("fetcing data"); // waiting slot will be shown$wait.end("fetching data"); // default slot will be shown
For detailed documentation, please visit https://github.com/f/vue-wait
Conclusion
When we examine “waiting” in detail, it is far from reality to implement the flow of the application with one “loading” type. Knowing the possibilities will increase the power you have. Also, as you can see in the code above, “waiting” is both application and component level state management.
Thanks Fatih Acet and Eser Ozvataf for review.
Discussion