Decorators are your friend

How I made higher order components easy.

Brian Engelhardt
5 min readFeb 21, 2017

Inheritance is dead. MVC is dead. Components for everything is the way to go. Large, mostly lateral object trees are the future, and for good reason. Functional composition is losing favor for highly understandable class syntax. So with all of that, how do we maintain composition that is so necessary to attach behavior at a point in our view tree?

There are a few different ways that can be used to handle getting data into your view.

  1. Introduce a base class that understands fetching data from your API. Subclass it and implement the required methods that define what to fetch, without knowing how or how to update. Base class invariably either uses state or some internal variable (data is common) and forceUpdate.
  2. Create a separate wrapping component which tracks it’s own fetch state, manages updates and passes data as well as manipulation functions as props on it’s child.
  3. Implement the behavior in specific “sentinel” components which are a mix of data and presentation, passing that data down as well as using it directly.

Each of these is partially wrong for a number of different reasons:

  1. Base class is only safe if it is extremely resilient and is well tested for its contract. That is a lot of overhead (more than usual number of tests is really needed). In short, a base class that is frequently used across your application is typically an instance of code smell. And one which is complex enough to handle asynchronous data fetching IS a code smell.
  2. This complexity is often not reusable, and more importantly it needs to be abstracted relatively well to be reused in any fashion. It requires you to write two components for one output.
  3. These sentinel components are undoubtedly going to be at the wrong place, forcing you to write a lot of duplicate code and to pass props all the way down way too often.

Fortunately there is a better way: Higher order components.

Concretely, a higher-order component is a function that takes a component and returns a new component. — from the facebook react docs

Higher order components let us attach additional functionality in a reusable way. In my current project, I’m using graphql and redux. They both provide higher order components for store access.

import React from 'react';
import {graphql} from 'apollo-react';
import { connect } from 'react-redux';
import {gql} from 'graphql-tag';

class Component extends React.Component {
render() {
return <div></div>;
}
}
const reduxAwareComponent = connect(state => ({}))(Component);

const apolloAwareComponent = graphql(gql`query{toDos{id}}`)(reduxAwareComponent);

export default apolloAwareComponent;

This pattern is getting to be the norm for react, which makes sense. After all, it works well, and when you use it, these higher order components are practically transparent. The only problem is they are NOT transparent in the editor. When I am consuming Component by importing it, I actually get apolloAwareComponent. My editor understands that is what I’m getting. But reality is that as a consumer, I don’t care how my component got its data.

When I consume this code, I don’t care one bit how Component gets at graphql or redux, or even that it does. I care that it provides me a doohickey to put on my page. What’s more, this code has a certain fragility. Let’s say that originally, I wasn’t using Redux anything in this particular component:

import React from 'react';
import {graphql} from 'apollo-react';
import {gql} from 'graphql-tag';

class Component extends React.Component {
render() {
return <div></div>;
}
}

const apolloAwareComponent = graphql(gql`query{toDos{id}}`)(Component);

export default apolloAwareComponent;

Well, that’s interesting. See, graphql directly wraps Component. Now to add Redux, I have to do three things:

  1. Import connect
  2. Wrap connect around the component
  3. Make graphql wrap the result of that.

One extra step multiplied by hundreds of components in an application adds up. (step 3 is an extra required because I usually need the redux store inside the graphql wrapper). Fortunately, there is a better way with decorators. In fact, you don’t even have to do anything significant to use them. Let’s start with the .babelrc for this sample:

{
"presets": [
"latest"
],
"plugins": [
"transform-decorators-legacy",
"transform-class-properties"
]
}

The name transform-decorators-legacy is worth noting. The decorator spec is in flux and may still change. If it does, these decorators may need to change. I intend to update this article when that changes, but I won’t make any promises.

With that, I can begin to use decorated components. Let’s start with just the graphql handler:

import React from 'react';
import {graphql} from 'apollo-react';
import {gql} from 'graphql-tag';

@graphql(gql`query{toDos{id}}`)
export default class Component extends React.Component {
render() {
return <div></div>;
}
}

I want to point out a few critical things here:

  • The export statement moved to the class which is the star of the file, not the wrapper. This means editors like Webstorm understand that Component is what you are using, not graphql
  • I don’t have to store the wrapped version anywhere, or do anything to construct it. This is all handled by transpilation now, the environment in the future.
  • This code is shorter, more concise, and clearly says what it is doing.
  • This maintains the compositional nature of javascript while allowing for well defined class like contracts.

Now let’s say I want to add redux between graphql and my component. Only two changes are needed. First, I import connect from redux. Second, I add a new decorator right after graphql.

import React from 'react';
import {graphql} from 'apollo-react';
import {gql} from 'graphql-tag';
import { connect } from 'react-redux';

@graphql(gql`query{toDos{id}}`)
@connect(state => ({}))
export default class Component extends React.Component {
render() {
return <div></div>;
}
}

Wasn’t that a bit cleaner? It also at no point changed what consumers of the component know or care. You can set proptypes on Component and webstorm will autocomplete those for you when using Component. It wouldn’t do that when you were dealing with the wrapped version. The beauty of this is that decorators provide a contract. The contract of that @ symbol is simple. The external meaning of the item being decorated is not fundamentally altered by it. There are a number of other uses for decorators, for example auto binding:

export default function Handler(target, name, descriptor) {
let bound = Symbol('bound' + name);
const value = descriptor.value || descriptor.initializer();
descriptor.get = function get() {
if (!this[bound]) {
this[bound] = value.bind(this);
}
return this[bound];
}
delete descriptor.value;
return descriptor;
}

This is used very simply:

import Handler from 'handler.js';
import React from 'react';
export default Component extends React.Component {
@Handler
onClick(event) {
console.log(this, event);
}
render() {
return <div onClick={this.onClick} />
}
}

Whenever that div is clicked, the console will log the component instance and the event. This is actually better than using an arrow function because it binds exactly once, not once per render, once per component. Additionally, it saves you from arrow functions all over your render function, which should be side effect free. the @Handler decorator assumes that it is being put on an instance function. This effectively provides early binding at the time the function is referenced, rather than at the time it is called.

What are you going to do with decorators? The possibilities are endless, but like any tool, it’s important not to go looking for things you could solve using decorators. After all, they are just one tool in the really cool toolbox that is modern Javascript.

This article is the first in what I hope will be a series on practical applications of lesser used ES6/ES7 features. All code samples can be found in this gist: https://gist.github.com/lassombra/616f5e1024093f539387f1fbc2fb709c

Articles in the series so far:

--

--