Not too long ago, I built a React app using MobX for state management. It was an exciting, at times confusing, but overall enjoyable experience that I plan to write about soon. One distinctive feature of MobX development that I found especially interesting was its use of decorators to annotate the properties of classes. I hadn’t really used them in JavaScript before, but after using the ones provided by MobX and writing a couple of my own, I think that they’re a feature with enormous potential.
Decorators aren’t a core feature of JavaScript yet; they’re working their way through ECMA TC39’s standardization process. That doesn’t mean we can’t get familiar with them, though. It looks like they’ll be supported natively by Node and browsers at some point in the near future, and in the meantime we’ve got Babel.
What is a Decorator?
No, not that kind of decorator. They’re designers, anyway.
Decorator is shorthand for “decorator function” (or method). It’s a function that modifies the behavior of the function or method passed to it by returning a new function. I said “function” a lot there. That’s an occupational hazard when you’re discussing higher-order functions.
You can implement decorators in any language that supports functions as first-class citizens, e.g. JavaScript, where you can bind a function to a variable or pass it as an argument to another function. A couple of those languages have special syntactic sugar for defining and using decorators; one of them is Python:
def cashify(fn):
def wrap():
print("$$$$")
fn()
print("$$$$")
return wrap
@cashify
def sayHello():
print("hello!")
sayHello()
# $$$$
# hello!
# $$$$
Let’s take a look at what’s going on there. Our cashify
function is a decorator: it receives a function as an argument, and its return value is also a function. We use Python’s “pie” syntax to apply the decorator to our sayHello
function, which is essentially the same thing as if we’d done this below the definition of sayHello
:
def sayHello():
print("hello!")
sayHello = cashify(sayHello)
The end result is that we print dollar signs before and after whatever we’re printing from the function we decorate.
Why am I introducing ECMAScript decorators using an example in Python? I’m glad you asked!
- Python is a great way to explain the basics because its concept of decorators is a bit more straightforward than the way they work in JS.
- JS and TypeScript both use Python’s “pie syntax” to apply decorators to methods and properties of classes, so it’s visually and syntactically similar.
Okay, what’s different about JS decorators?
JS Decorators and Property Descriptors
While Python decorators are passed whatever function they’re decorating as an argument, JS decorators receive quite a bit more information due to the way objects work in that language.
Objects in JS have properties, and those properties have values:
const oatmeal = {
viscosity: 20,
flavor: 'Brown Sugar Cinnamon',
};
But in addition to its value, each property has a bunch of other behind-the-scenes information that defines different aspects of how it works, called a property descriptor:
console.log(Object.getOwnPropertyDescriptor(oatmeal, 'viscosity'));
/*
{
configurable: true,
enumerable: true,
value: 20,
writable: true
}
*/
JS is tracking quite a few things related to that property:
configurable
determines whether or not the type of the property can be changed, and whether it can be deleted from the object.
enumerable
controls whether the property shows up when you enumerate the object’s properties (like when you call Object.keys(oatmeal)
or use it in a for
loop).
writable
controls whether or not you can change the property’s value via the assignment operator =
.
value
is the static value of the property that you see when you access it. It’s usually the only part of the property descriptor that you see or are concerned with. It can be any JS value, including a function, which would make the property a method of the object it belongs to.
Property descriptors can also have two other properties that cause JS to treat them as “accessor descriptors” (more commonly known as getters and setters):
get
is a function that returns the property’s value instead of using the static value
property.
set
is a special function that gets passed whatever you put on the right side of the equals sign as an argument when you assign a value to the property.
Decorating Without the Frills
JS has actually had an API for working with property descriptors since ES5, in the form of the Object.getOwnPropertyDescriptor
and Object.defineProperty
functions. For example, If I like the thickness of my oatmeal just the way it is, I can make it read-only using that API like so:
Object.defineProperty(oatmeal, 'viscosity', {
writable: false,
value: 20,
});
// When I try to set oatmeal.viscosity to a different value, it'll just silently fail.
oatmeal.viscosity = 30;
console.log(oatmeal.viscosity);
// => 20
I can even write a generic decorate
function that lets me mess with the descriptor for any property of any object:
function decorate(obj, property, callback) {
var descriptor = Object.getOwnPropertyDescriptor(obj, property);
Object.defineProperty(obj, property, callback(descriptor));
}
decorate(oatmeal, 'viscosity', function(desc) {
desc.configurable = false;
desc.writable = false;
desc.value = 20;
return desc;
});
Adding the Shiplap and Crown Molding
The first major difference with the Decorators proposal is that it only concerns itself with ECMAScript classes, not regular objects. We’re going to need to over-engineer our breakfast in order to really demonstrate what we can accomplish, so let’s make some classes to represent our bowl of porridge:
class Porridge {
constructor(viscosity = 10) {
this.viscosity = viscosity;
}
stir() {
if (this.viscosity > 15) {
console.log('This is pretty thick stuff.');
} else {
console.log('Spoon goes round and round.');
}
}
}
class Oatmeal extends Porridge {
viscosity = 20;
constructor(flavor) {
super();
this.flavor = flavor;
}
}
We’re representing our bowl of oatmeal using a class that inherits from the more generic Porridge
class. Oatmeal
sets the default viscosity higher than Porridge
‘s default, and it adds a new flavor
property. We’re also using another ECMAScript proposal, class fields, to override the viscosity
value.
We can re-create our original bowl of oatmeal like so:
const oatmeal = new Oatmeal('Brown Sugar Cinnamon');
/*
Oatmeal {
flavor: 'Brown Sugar Cinnamon',
viscosity: 20
}
*/
Great, we’ve got our ES6 oatmeal, and we’re ready to write a decorator!
How to Write a Decorator
JS decorator functions are passed three arguments:
target
is the class that our object is an instance of.
key
is the property name, as a string, that we’re applying the decorator to.
descriptor
is that property’s descriptor object.
What we do inside of the decorator function depends on the purpose of our decorator. In order to decorate a method or property of an object, we need to return a new property descriptor. Here’s how we can write a decorator that makes a property read-only:
function readOnly(target, key, descriptor) {
return {
...descriptor,
writable: false,
};
}
We’d use it by modifying our Oatmeal class like this:
class Oatmeal extends Porridge {
@readOnly viscosity = 20;
// (you can also put @readOnly on the line above the property)
constructor(flavor) {
super();
this.flavor = flavor;
}
}
Now our oatmeal’s glue-like consistency is immune to tampering. Thank goodness.
What if we want to do something that’s actually useful? I ran into a situation while working on a recent project where a decorator saved me a lot of typing and maintenance overhead:
Handling API Errors
In the MobX/React app I mentioned in the beginning, I have a couple of different classes that act as data stores. They each represent collections of different things that the user interacts with, and they each talk to different API endpoints for data from the server. In order to handle API errors, I made each of the stores follow a protocol when communicating over the network:
- Set the UI store’s
networkStatus
property to “loading.”
- Send a request to the API
- Handle the result
- If successful, update local state with the response
- If something goes wrong, set the UI store’s
apiError
property to the error we received
- Set the UI store’s
networkStatus
property to “idle.”
I found myself repeating this pattern a few times before I noticed the smell:
class WidgetStore {
async getWidget(id) {
this.setNetworkStatus('loading');
try {
const { widget } = await api.getWidget(id);
// Do something with the response to update local state:
this.addWidget(widget);
} catch (err) {
this.setApiError(err);
} finally {
this.setNetworkStatus('idle');
}
}
}
That’s a lot of error handling boilerplate. I decided that since I was already using MobX’s @action
decorators on all the methods that updated observable properties (not shown here for the sake of simplicity), I might as well just tack on an additional decorator that allowed me to recycle my error handling code. I came up with this:
function apiRequest(target, key, descriptor) {
const apiAction = async function(...args) {
// More about this line shortly:
const original = descriptor.value || descriptor.initializer.call(this);
this.setNetworkStatus('loading');
try {
const result = await original(...args);
return result;
} catch (e) {
this.setApiError(e);
} finally {
this.setNetworkStatus('idle');
}
};
return {
...descriptor,
value: apiAction,
initializer: undefined,
};
}
I could then replace the boilerplate that I was writing in each API action method with something like this:
class WidgetStore {
@apiRequest
async getWidget(id) {
const { widget } = await api.getWidget(id);
this.addWidget(widget);
return widget;
}
}
My error handling code is still there, but now I only need to write it once and ensure that each class that uses it has a setNetworkStatus
and setApiError
method.
A Babel Workaround
So what’s up with that line where I’m choosing between descriptor.value
and calling descriptor.initializer
? That’s a Babel thing. My hunch is that it won’t work that way when JS supports decorators natively, but it’s necessary right now because of how Babel handles arrow functions defined as class properties.
When you define a class property and assign an arrow function as its value, Babel does a little trick to bind that function to the correct instance of the class and give you the right this
value. It does this by setting descriptor.initializer
to a function that returns the function you wrote, with the correct this
value in its scope.
An example should make things less muddy:
class Example {
@myDecorator
someMethod() {
// In this case, our method would be referred to by descriptor.value
}
@myDecorator
boundMethod = () => {
// Here, descriptor.initializer would be a function that, when called, would return our `boundMethod` function, properly scoped so that `this` refers to the current instance of Example.
};
}
Decorating Classes
In addition to properties and methods, you can also decorate an entire class. In order to do that, you really only need the first argument passed to your decorator function, target
. For example, I can write a decorator that automatically registers the class it’s wrapping as a custom HTML element. I’m using a closure here to enable the decorator to receive whatever name we want to give the element as an argument:
function customElement(name) {
return function(target) {
customElements.define(name, target);
};
}
We’d use it like this:
@customElement('intro-message');
class IntroMessage extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
this.wrapper = this.createElement('div', 'intro-message');
this.header = this.createElement('h1', 'intro-message__title');
this.content = this.createElement('div', 'intro-message__text');
this.header.textContent = this.getAttribute('header');
this.content.innerHTML = this.innerHTML;
shadow.appendChild(this.wrapper);
this.wrapper.appendChild(this.header);
this.wrapper.appendChild(this.content);
}
createElement(tag, className) {
const elem = document.createElement(tag);
elem.classList.add(className);
return elem;
}
}
Load that into our HTML, and we can use it like this:
<intro-message header="Welcome to Decorators">
<p>Something something content...</p>
</intro-message>
Which gives us this in the browser:
Wrapping Up
Using decorators in your projects today requires some transpiler configuration. The most straightforward guide that I’ve seen is located in the MobX docs. It has info for TypeScript and two major versions of Babel.
Keep in mind that decorators are an evolving proposal at this point, so if you use them in production code now, you’ll probably either need to make some updates or keep using Babel’s decorators plugin in legacy mode once they become an official part of the ECMAScript specification. While it’s not even well-supported by Babel yet, the latest version of the decorators proposal already contains big changes that are not backward compatible with the previous version.
Decorators, like many bleeding edge JS features, are a useful tool to have in your kit. They can greatly simplify the sharing of behavior across different, unrelated classes. However, there’s always a cost associated with early adoption. Use decorators, but do so with a clear idea of the implications for your codebase.
The post Understanding JavaScript Decorators appeared first on Simple Thread.