LoginSignup
28
13

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-03-03

React #1 Advent Calendar 2017が埋まっていなかったので投稿。

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

Decoratorsとは

  • クラス、メソッド、アクセッサ、プロパティ、パラメーターをラップして処理の変更・追加をしたり、値を変更することができます。
  • Decorator関数はクラスやメソッドに対して@hogehogeのように@を付けて使います。
  • Javaのアノテーションに似てますが実際Java由来だそうです。
  • 名前がGoFのデコレーターパターンに似てますが、実際デコレーターパターン由来だそうです。
  • 執筆時点(2018/03/01)ではstage-2です
  • 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を見てください。

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>
    )
  }
}

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

参考

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

28
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
13