Understanding Decorators

RelativeCI - In-depth bundle stats analysis and monitoring - Interview with Viorel Cojocaru

If you have used languages, such as Java or Python before, you might be familiar with the idea. Decorators are syntactic sugar that allow us to wrap and annotate classes and functions. In their current proposal (stage 1) only class and method level wrapping is supported. Functions may become supported later on.

In Babel 6 you can enable this behavior through babel-plugin-syntax-decorators and babel-plugin-transform-decorators-legacy plugins. The former provides syntax level support whereas the latter gives the type of behavior we are going to discuss here.

The greatest benefit of decorators is that they allow us to wrap behavior into simple, reusable chunks while cutting down the amount of noise. It is definitely possible to code without them. They just make certain tasks neater, as we saw with drag and drop related annotations.

Implementing a Logging Decorator#

Sometimes, it is useful to know how methods are being called. You could of course attach console.log there but it's more fun to implement @log. That's a more controllable way to deal with it. Consider the example below:

class Math {
  @log
  add(a, b) {
    return a + b;
  }
}

function log(target, name, descriptor) {
  var oldValue = descriptor.value;

  descriptor.value = function() {
    console.log(`Calling "${name}" with`, arguments);

    return oldValue.apply(null, arguments);
  };

  return descriptor;
}

const math = new Math();

// passed parameters should get logged now
math.add(2, 4);

The idea is that our log decorator wraps the original function, triggers a console.log, and finally, calls it again while passing the original arguments to it. Especially if you haven't seen arguments or apply before, it might seem a little strange.

apply can be thought as an another way to invoke a function while passing its context (this) and parameters as an array. arguments receives function parameters implicitly so it's ideal for this case.

This logger could be pushed to a separate module. After that, we could use it across our application whenever we want to log some methods. Once implemented decorators become powerful building blocks.

The decorator receives three parameters:

  • target maps to the instance of the class.
  • name contains the name of the method being decorated.
  • descriptor is the most interesting piece as it allows us to annotate the method and manipulate its behavior. It could look like this:
const descriptor = {
  value: () => {...},
  enumerable: false,
  configurable: true,
  writable: true
};

As you saw above, value makes it possible to shape the behavior. The rest allows you to modify behavior on method level. For instance, a @readonly decorator could limit access. @memoize is another interesting example as that allows you to implement easy caching for methods.

Implementing @connect#

@connect will wrap our component in another component. That, in turn, will deal with the connection logic (listen/unlisten/setState). It will maintain the store state internally and then pass it to the child component that we are wrapping. During this process, it will pass the state through props. The implementation below illustrates the idea:

app/decorators/connect.js

import React from 'react';

const connect = (Component, store) => {
  return class Connect extends React.Component {
    constructor(props) {
      super(props);

      this.storeChanged = this.storeChanged.bind(this);
      this.state = store.getState();

      store.listen(this.storeChanged);
    }
    componentWillUnmount() {
      store.unlisten(this.storeChanged);
    }
    storeChanged() {
      this.setState(store.getState());
    }
    render() {
      return <Component {...this.props} {...this.state} />;
    }
  };
};

export default (store) => {
  return (target) => connect(target, store);
};

Can you see the wrapping idea? Our decorator tracks store state. After that, it passes the state to the component contained through props.

... is known as a spread operator. It expands the given object to separate key-value pairs, or props, as in this case.

You can connect the decorator with App like this:

app/components/App.jsx

...
import connect from '../decorators/connect';

...

@connect(NoteStore)
export default class App extends React.Component {
  render() {
    const notes = this.props.notes;

    ...
  }
  ...
}

Pushing the logic to a decorator allows us to keep our components simple. If we wanted to add more stores to the system and connect them to components, it would be trivial now. Even better, we could connect multiple stores to a single component easily.

Decorator Ideas#

We can build new decorators for various functionalities, such as undo, in this manner. They allow us to keep our components tidy and push common logic elsewhere out of sight. Well designed decorators can be used across projects.

Alt's @connectToStores#

Alt provides a similar decorator known as @connectToStores. It relies on static methods. Rather than normal methods that are bound to a specific instance, these are bound on class level. This means you can call them through the class itself (i.e., App.getStores()). The example below shows how we might integrate @connectToStores into our application.

...
import connectToStores from 'alt-utils/lib/connectToStores';

@connectToStores
export default class App extends React.Component {
  static getStores(props) {
    return [NoteStore];
  };
  static getPropsFromStores(props) {
    return NoteStore.getState();
  };
  ...
}

This more verbose approach is roughly equivalent to our implementation. It actually does more as it allows you to connect to multiple stores at once. It also provides more control over the way you can shape store state to props.

Conclusion#

Even though still a little experimental, decorators provide nice means to push logic where it belongs. Better yet, they provide us a degree of reusability while keeping our components neat and tidy.

Previous chapter
Language Features
Next chapter
Troubleshooting

This book is available through Leanpub. By purchasing the book you support the development of further content.

Need help?