React #1 Advent Calendar 2017が埋まっていなかったので投稿。
EcmaScript proposalにDecoratorsという新機能があります。
まだまだ仕様策定中ですが、先取りしてReactのコンポーネントを拡張する方法について紹介します。
Decoratorsとは
- クラス、メソッド、アクセッサ、プロパティ、パラメーターをラップして処理の変更・追加をしたり、値を変更することができます。
- Decorator関数はクラスやメソッドに対して
@hogehoge
のように@
を付けて使います。 - Javaのアノテーションに似てますが実際Java由来だそうです。
- 参考: JavaScript Decorators
- 名前がGoFのデコレーターパターンに似てますが、実際デコレーターパターン由来だそうです。
- 参考: JavaScript Decorators
- 執筆時点(2018/03/01)ではstage-2です
- 参考: Decorators proposal
- AOP。アスペクト指向的な使い方ができます。
Decoratorsの使い方
JavaScript Decoratorsを使うためにはbabelとTypeScriptを使う方法があります。
babel
babelではbabel-plugin-transform-decorators-legacyを使います。
なぜlegacyなのかは参考記事(欅樹雑記: ECMAScriptでメソッド呼び出し時に引数をいじるデコレータ)をお読み下さい。
- インストール
npm install --save-dev babel-plugin-transform-decorators-legacy
- .babelrcにプラグインを追加
{
"plugins": ["transform-decorators-legacy"]
}
TypeScript
tscでトランスパイルするときに--experimentalDecorators
をつけます。
参考: Decorators · TypeScript
tsc --experimentalDecorators
Decoratorsを使ってコンポーネントを拡張する
Decoratorsを使ってコンポーネントの振る舞いやプロパティを変更することができます。
例えば、いくつかのコンポーネントに共通の関数を定義したい時や共通の値を持たせたい場合などにも使えます。
プロパティを追加する
簡単な例でいうと共通のプロパティを持たせたい場合、以下のようなDecorator関数を定義しておきクラスにアノテーションすれば実現します。
クラスのプロパティはprototypeに生やせばいいだけです。
Decorator関数の第一引数にはClass functionが渡ってくるのでその引数のprototypeにプロパティを追加します。
- コンポーネントにプロパティを追加するDecorator関数
function greeting(target) {
target.prototype.name = "Decorator";
}
- Decorator関数を使ったコンポーネント
@greeting // <= Decorators function!!
class App extends React.Component {
render() {
return (
<div>
{`Hello ${this.name}!`}
</div>
);
}
}
コンポーネント自体にはname
というプロパティは定義されていませんが、Decorator関数によって定義されているため、参照できます。
表示は以下codepenで確認ください。
See the Pen React + Decorator sample by Masashi Hirano (@shisama) on CodePen.
メソッドを置き換える
次はコンポーネントのメソッドを置き換える方法を紹介をします。
要領はプロパティと同じで、prototypeの中にメソッド定義があるので変更するだけです。
例えば、render関数を置き換えたいなら以下のようなDecorator関数を作ります。
- renderを置き換えるDecorator関数
function greeting(target) {
target.prototype.render = function() {
return <h1>Good Morning!</h1>
};
}
- Decorator関数を使ったコンポーネント
@greeting // <= Decorators function!!
class App extends React.Component {
render() {
return (
<div>
Hello!
</div>
);
}
}
表示は以下のcodepenで確認ください
See the Pen React + Decorator sample render by Masashi Hirano (@shisama) on CodePen.
shouldComponentUpdateを置き換える
ライブラリを作りました。
PureComponentを継承することでshouldComponentUpdate内でshallow equalしてくれますが、deep equalしたいことがあったので、Decoratorsを使ったライブラリをつくりました。
npm install pure-deep-equal
で入ります。
ソースコードはshisama/pure-deep-equalを見てください。
@PureDeepEqual
class Test extends React.Component {
render() {
return <span>{this.props.message}</h1>
}
}
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関数
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が呼ばれるたびにログを表示
class App extends Component {
@log
render() {
return (
<div>
<input type="text" onChange={this.props.onChange} />
<p>{this.props.message}</p>
</div>
)
}
}
これまでのDecorator関数と違い引数を3つとります。
ポイントは第3引数のプロパティディスクリプタのvalueを書き換えるところです。
メソッドが呼ばれたときにpropsやstateの値を表示する
ライブラリを作りました。
上記のようにコンポーネント内に定義しているメソッドが呼ばれるたびにpropsやstateの内容をコンソールに表示するロガーライブラリを作ったので紹介したいと思います。
npm install react-log-decorator
で入ります。
ソースコードはshisama/react-log-decoratorを見てください。
このDecoratorは以下のようにrenderやcomponentDidMountなどに使えます。
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>
)
}
}
デバッグするときなんかに役立つかもしれませんので、使ってみて気に入ればスターいただけると嬉しいです!
参考
- JavaScript Decorators - Qiita
- 全力で ES Decorator使ってみた - Qiita
- 欅樹雑記: ECMAScriptでメソッド呼び出し時に引数をいじるデコレータ
- Decorators proposal
- Decorators · TypeScript
- Decorators transform · Babel
- loganfsmyth/babel-plugin-transform-decorators-legacy: A plugin for Babel 6 that (mostly) replicates the old decorator behavior from Babel 5
最後までお読みいただきありがとうございました。
不備や不明点があれば、お手数おかけいたしますがコメント欄やTwitterなどからお願い致します。