Loaders are one of the basic functionalities that appear in applications and which are of great importance for user experience. Their purpose is to show the user that the application performs a task in the background that requires a moment of patience. An exemplary use case is downloading data from a server – we send a query to the server and wait for a response. Frontend-backend communication is performed very often and from different places in the frontend application, so the loader is displayed regularly. From the developer’s point of view, we would like the display management of this component to be as simple as possible – in my case, it was limited to adding a loader only in one place of the application and managing it through the Vuex store. The only difficulty I had to face was to inform neatly when to display and when to remove a loader. Help came with TypeScript functionality – decorators. About them, I would like to tell you and present a full implementation of the loader in my application using them.
What are decorators?
Using the TypeScript documentation:
A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration.
We see that with the help of a decorator we can expand the possibilities and modify the properties of e.g. classes or methods.
When implementing the loader, we will need a decorator for methods, so I will focus on how it works for them. Let’s create a MyClass class that will have three methods: foo, bar, baz. In the console, we would like to see which method was called, but without littering the bodies of these methods. This is an excellent use case for a decorator, which we can use to log calls.
class MyClass {
@log
foo(): void {}
@log
bar(): void {}
@log
baz(): void {}
}
function log(target: Function, propertyKey: string, descriptor: PropertyDescriptor): void {
console.log('Called', propertyKey);
}
We see that the class itself does not have to implement console login. It can take care of its responsibility, and the decorator will take care of the rest. There are also decorator factories that return the function performed in runtime. Their advantage is that they can pass the value to the decorator:
function customizableLog(shouldLog: boolean):
(target: Function, propertyKey: string, descriptor: PropertyDescriptor) => void
{
return function(target: Function, propertyKey: string, descriptor: PropertyDescriptor) {
shouldLog && console.log('Called', propertyKey);
}
}
In the class we use it like this:
@customizableLog(false)
foo(): void {}
@customizableLog(true)
bar(): void {}
Let’s explain what the arguments passed to decorators are. The first of these, target, is a reference to the prototype of a class for instance methods or a reference to the constructor for static methods of this class. The second, propertyKey, contains the name of the decorated method, and the last, descriptor is the object implementing the interface:
interface PropertyDescriptor {
configurable?: boolean;
enumerable?: boolean;
value?: any;
writable?: boolean;
get?(): any;
set?(v: any): void;
}
You can read more about the attributes of this object here. The value will be the most important attribute for us. It is there that the reference to the decorated method in the class is stored, so we can even overwrite it with new implementations. Let’s look at an example:
class MyClass {
@changeMethod(true)
foo() {
console.log('Foo');
}
@changeMethod(false)
bar() {
console.log('Bar');
}
}
function changeMethod(shouldChange: boolean):
(target: Function, propertyKey: string, descriptor: PropertyDescriptor) => void
{
return function(target: Function, propertyKey: string, descriptor: PropertyDescriptor) {
if (shouldChange) {
descriptor.value = function() {
console.log('Changed');
}
}
}
}
If we create an instance of the >MyClass class and call individual methods, we get
const myClass = new MyClass();
myClass.foo(); // 'Changed'
myClass.bar(); // 'Bar'
Knowing how decorators work, let’s get to the main task – to create a loader that will appear when sending queries to the server. I will use a simple application presenting this functionality.
Setup
The setup for this application looks like this:
- Vue 2.6.10
- TypeScript 3.4.3
- Vuex 3.0.1
- fetch API
I will present individual files together with an explanation of their content. App.vue component
// App.vue
The main component of the project. This is where I nest the loader component, which displays via v-if is based on the logical value isLoading. This value is stored in the vuex store. Also, I add Posts component, which is the root of the application. Posts component
// Posts.vue
List of posts' titles
-
{{ post.title }}
A functional component whose task is to download data (posts) and display their titles in a list. To retrieve data, the component creates an API service instance and calls its method to query the server for resources.
API service
// api.service.ts
export class ApiService {
@showLoader
public async fetchData(): Promise {
return fetch('https://jsonplaceholder.typicode.com/posts')
.then((response) =>; response.json());
}
}
The fetch data method from the service is responsible for sending a query to the server and passing the value to the component. Here also the first important thing happens - I decorate this method with the showLoader function.
Store
// store.ts
const actions: ActionTree = {
async showLoader({ commit }: { commit: Commit }) {
commit('showLoading');
},
async hideLoader({ commit }: { commit: Commit }) {
commit('hideLoading');
}
};
const mutations: MutationTree = {
showLoading(state: RootState) {
state.isLoading = true;
},
hideLoading(state: RootState) {
state.isLoading = false;
}
};
export default new Vuex.Store({
state: {
isLoading: false,
},
mutations,
actions,
});
Store contains state, actions and mutations for the loader component. The state stores the isLoading value, based on which I display the loader. Actions cause specific changes in the state through mutations, in this case, they change the value of isLoading.
showLoader decorator
// show-loader.ts
import store from '@/store';
export function showLoader(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const classMethod = descriptor.value; // fetchData method in our case
descriptor.value = function() {
store.dispatch('showLoader');
// call original method with proper context and arguments passed to it
return classMethod.apply(this, arguments)
// intercept response to hide loader
.then((v: any) => {
store.dispatch('hideLoader');
return v;
})
.catch((err: any) => {
store.dispatch('hideLoader');
return Promise.reject(err);
});
};
}
The most important things are happening here. First, we need to import store to be able to communicate with it. In the beginning, in the body of the decorator, we write a reference to the class method because in the next step we overwrite the method with a new function. It is stored in the value field of the descriptor object describing the decorated method. The new function first calls the show loader action. Then we would like to call the right class method - and we do it using the built-in JavaScript apply method, which provides us with:
* the appropriate context this (in this case being a reference to an instance of the class) at the time of invocation,
* passing arguments to the class method.
Calling the function named classMethod returns Promise, to which we can connect regardless of whether it is satisfied or rejected, just to hide the loader and pass the obtained value. Thanks to this decorator, we achieve the intended effect - the application informs about data processing at the right time, and we also separated the responsibility of downloading data from the API and displaying the loader. The above implementation does not include the case when more than one server query is performed - currently, the loader will disappear after the first query has been completed. Implementation considering this case can be found on my github.
Summary
Decorators helped me easily solve the problem of displaying the loader throughout the application. They are interesting and uncomplicated functionality, giving many possibilities. In my case, they significantly improved the readability of the code, separating responsibilities between components, store and API. It's worth remembering about them and getting used to them.