DecoratorsでReactコンポーネントを拡張する

[React #1 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/react)が埋まっていなかったので投稿。

EcmaScript proposalにDecoratorsという新機能があります。
まだまだ仕様策定中ですが、先取りしてReactのコンポーネントを拡張する方法について紹介します。

# Decoratorsとは
* クラス、メソッド、アクセッサ、プロパティ、パラメーターをラップして処理の変更・追加をしたり、値を変更することができます。
* Decorator関数はクラスやメソッドに対して`@hogehoge`のように`@`を付けて使います。
* Javaのアノテーションに似てますが実際Java由来だそうです。
 * 参考: [JavaScript Decorators](https://qiita.com/armorik83/items/e3a0ce67f569ddc4b432#decoratorsの文化的背景)
* 名前がGoFのデコレーターパターンに似てますが、実際デコレーターパターン由来だそうです。
 * 参考: [JavaScript Decorators](https://qiita.com/armorik83/items/e3a0ce67f569ddc4b432#decoratorsの文化的背景)
* 執筆時点(2018/03/01)ではstage-2です
 * 参考: [Decorators proposal](https://tc39.github.io/proposal-decorators/)
* AOP。アスペクト指向的な使い方ができます。

# Decoratorsの使い方
JavaScript Decoratorsを使うためにはbabelとTypeScriptを使う方法があります。

## babel
babelでは[babel-plugin-transform-decorators-legacy](https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy)を使います。
なぜlegacyなのかは参考記事([欅樹雑記: ECMAScriptでメソッド呼び出し時に引数をいじるデコレータ](http://blog.zelkova.cc/2017/08/decorator-modify-arguments.html))をお読み下さい。

- インストール

```bash
npm install --save-dev babel-plugin-transform-decorators-legacy
```

- .babelrcにプラグインを追加

```json
{
  "plugins": ["transform-decorators-legacy"]
}
```

## TypeScript
tscでトランスパイルするときに`--experimentalDecorators`をつけます。
参考: [Decorators &middot; TypeScript
](https://www.typescriptlang.org/docs/handbook/decorators.html)

```bash
tsc --experimentalDecorators
```

# Decoratorsを使ってコンポーネントを拡張する

Decoratorsを使ってコンポーネントの振る舞いやプロパティを変更することができます。

例えば、いくつかのコンポーネントに共通の関数を定義したい時や共通の値を持たせたい場合などにも使えます。

## プロパティを追加する

簡単な例でいうと共通のプロパティを持たせたい場合、以下のようなDecorator関数を定義しておきクラスにアノテーションすれば実現します。
クラスのプロパティはprototypeに生やせばいいだけです。
Decorator関数の第一引数にはClass functionが渡ってくるのでその引数のprototypeにプロパティを追加します。

- コンポーネントにプロパティを追加するDecorator関数

```js
function greeting(target) {
  target.prototype.name = "Decorator";
}
```

- Decorator関数を使ったコンポーネント

```jsx
@greeting // <= Decorators function!!
class App extends React.Component {
  render() {
    return (
      <div>
        {`Hello ${this.name}!`}
      </div>
    );
  }
}
```

コンポーネント自体には`name`というプロパティは定義されていませんが、Decorator関数によって定義されているため、参照できます。

表示は以下codepenで確認ください。

<p data-height="265" data-theme-id="0" data-slug-hash="rJPqVq" data-default-tab="js,result" data-user="shisama" data-embed-version="2" data-pen-title="React + Decorator sample" class="codepen">See the Pen <a href="https://codepen.io/shisama/pen/rJPqVq/">React + Decorator sample</a> by Masashi Hirano (<a href="https://codepen.io/shisama">@shisama</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

## メソッドを置き換える

次はコンポーネントのメソッドを置き換える方法を紹介をします。
要領はプロパティと同じで、prototypeの中にメソッド定義があるので変更するだけです。
例えば、render関数を置き換えたいなら以下のようなDecorator関数を作ります。

- renderを置き換えるDecorator関数

```js
function greeting(target) {
  target.prototype.render = function() {
    return <h1>Good Morning!</h1>
  };
}
```


- Decorator関数を使ったコンポーネント

```jsx
@greeting // <= Decorators function!!
class App extends React.Component {
  render() {
    return (
      <div>
        Hello!
      </div>
    );
  }
}
```

表示は以下のcodepenで確認ください

<p data-height="265" data-theme-id="0" data-slug-hash="NyoEYV" data-default-tab="js,result" data-user="shisama" data-embed-version="2" data-pen-title="React + Decorator sample render" class="codepen">See the Pen <a href="https://codepen.io/shisama/pen/NyoEYV/">React + Decorator sample render</a> by Masashi Hirano (<a href="https://codepen.io/shisama">@shisama</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

## shouldComponentUpdateを置き換える
ライブラリを作りました。
PureComponentを継承することでshouldComponentUpdate内でshallow equalしてくれますが、deep equalしたいことがあったので、Decoratorsを使ったライブラリをつくりました。
`npm install pure-deep-equal`で入ります。
ソースコードは[shisama/pure-deep-equal](https://github.com/shisama/pure-deep-equal)を見てください。

```jsx
@PureDeepEqual
class Test extends React.Component {
  render() {
    return <span>{this.props.message}</h1>
  }
}
```

```jsx
class Test extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return !deepEqual(this.props, nextProps) || !deepEqual(this.state, nextState);
  }

  render() {
    return <span>{this.props.message}</h1>
  }
}
```
上のDecoratorsを付けたコンポーネントは下のshouldComponentUpdateを実装した状態と同等になります。

## メソッドごとに処理を追加する
最後にメソッドごとに処理を追加するDecorator関数の作り方について紹介します。
これまではClass functionのprototypeに追加・置き換えを行ってきましたが、メソッドごとに処理を追加する方法は少し実装方法が変わります。

例えば、メソッドが呼ばれるたびにpropsやstateの値をコンソールに表示するには以下のようにします。

- メソッドに処理を追加するDecorator関数

```js
function log(target, name, descriptor) {
  const func = descriptor.value;
  descriptor.value = function (...args) {
    const log = console.log;
    log("props:" + JSON.stringify(this.props));
    log("state:" + JSON.stringify(this.state));
    return func.bind(this)(...args); |
  };
  return descriptor;
}
```

- renderが呼ばれるたびにログを表示

```jsx
class App extends Component {
  @log
  render() {
    return (
      <div>
        <input type="text" onChange={this.props.onChange} />
        <p>{this.props.message}</p>
      </div>
    )
  }
}
```

これまでのDecorator関数と違い引数を３つとります。
ポイントは第３引数のプロパティディスクリプタのvalueを書き換えるところです。

## メソッドが呼ばれたときにpropsやstateの値を表示する
ライブラリを作りました。
上記のようにコンポーネント内に定義しているメソッドが呼ばれるたびにpropsやstateの内容をコンソールに表示するロガーライブラリを作ったので紹介したいと思います。
`npm install react-log-decorator` で入ります。
ソースコードは[shisama/react-log-decorator](https://github.com/shisama/react-log-decorator)を見てください。

![react-log-decorator](https://github.com/shisama/react-log-decorator/blob/master/capture/react-log-decorator.gif?raw=true)

このDecoratorは以下のようにrenderやcomponentDidMountなどに使えます。

```jsx
import {Component} from 'react';
import logger from 'react-log-decorator';
const log = logger(process.env.NODE_ENV === 'development');

export default class MyComponent extends Component {
  @log
  render() {
    return (
      <div>
        <input type="text" onChange = {this.props.onChange} />
        <p>{this.props.message}</p>
      </div>
    )
  }
}
```

デバッグするときなんかに役立つかもしれませんので、使ってみて気に入ればスター:star:いただけると嬉しいです！

# 参考
* [JavaScript Decorators - Qiita](https://qiita.com/armorik83/items/e3a0ce67f569ddc4b432)
* [全力で ES Decorator使ってみた - Qiita](https://qiita.com/mizchi/items/6bdf9d100f564a5c5b08)
* [欅樹雑記: ECMAScriptでメソッド呼び出し時に引数をいじるデコレータ](http://blog.zelkova.cc/2017/08/decorator-modify-arguments.html)
* [Decorators proposal](https://tc39.github.io/proposal-decorators/)
* [Decorators &middot; TypeScript
](https://www.typescriptlang.org/docs/handbook/decorators.html)
* [Decorators transform · Babel](https://babeljs.io/docs/plugins/transform-decorators/)
* [loganfsmyth/babel-plugin-transform-decorators-legacy: A plugin for Babel 6 that (mostly) replicates the old decorator behavior from Babel 5](https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy)

最後までお読みいただきありがとうございました。
不備や不明点があれば、お手数おかけいたしますがコメント欄やTwitterなどからお願い致します。