Reactビギナーズガイドをtypescriptで勉強し直してわかったこと①【mixinは使うべからず(というか使えない)】

自己紹介と今更ながらreactの勉強を一から

私のことをまずお伝えすると、ここ数年は、とあるベンチャー企業のシステム部のマネージャーをしていました。

reactが出た時はなんだjqueryみたいな便利なライブラリでしょ、jsxなんてテンプレートエンジンでしょ、ぐらいにしか思っていなかったのが、今や、es6だのtypescriptだのgulpだのwebpackだの知らない用語がいっぱいでてきて、jsのフロント周りの開発についていけなくなってしまった浦島太郎sのうちの一人です。

今やhtml、さらにはjavascriptやcssですらベタ書かない時代になっていて、「俺がわからないライブラリは使うな!トランスパイラなんて信用できるか!!」なんて言えるはずもなく、さすがに現場のエンジニアとの意思疎通に支障を来すので、勉強することに。

オライリージャパンの「Reactビギナーズガイド」を買ってみる

https://www.oreilly.co.jp/books/9784873117881/
上の小鳥さんを購入しました。

本で紹介されているgithubにはfirstcommitが2016年5月にあるみたい。
調べたところ、es5によるコーディングは時代遅れのようで、じゃあ折角だからということで、どうも流行りらしい次の環境にて書いてある内容に取り組んでみました。

目標1. typescriptを使う

サーバもフロントもms製のtypescriptがどうも現場に聞いたらナウいようなので使ってみることに。javaとかc#っぽい印象を受けました。
「これならjavascriptじゃなくてjavaでフロントが動くようになればいいじゃん」と内心思いました(がそっと胸の内にしまっておきました)。

目標2. Visual Studio Codeを使う

こちらもms製。これの何がいいってなんのプラグインなしでもtypescript、jsxともにキレーに表示してくれてシンタックスエラーも適切に表示してくれる。

目標3. webpackを使う

本ではbrowserifyでのbundle方法が載っていたのですが、最近の流行りはwebpackのようなのでこれも流行りに乗ってみました。

結局npm i -g create-react-appすることに

結局は
https://github.com/Microsoft/TypeScript-React-Starter
に記載されている内容に即して開発環境を構築すればほとんどの目標が達成できることがわかりました。

create-react-app my-app --scripts-version=react-scripts-ts

で一発ですね。

出来上がった環境は次の通り:

package.json
{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.2.0",
    "react-dom": "^16.2.0",
    "react-scripts-ts": "2.13.0"
  },
  "scripts": {
    "start": "react-scripts-ts start",
    "build": "react-scripts-ts build",
    "test": "react-scripts-ts test --env=jsdom",
    "eject": "react-scripts-ts eject"
  },
  "devDependencies": {
    "@types/enzyme": "^3.1.8",
    "@types/jest": "^22.1.2",
    "@types/node": "^9.4.5",
    "@types/react": "^16.0.36",
    "@types/react-dom": "^16.0.3",
    "enzyme": "^3.3.0",
    "react-addons-test-utils": "^15.6.2",
    "typescript": "^2.7.1"
  }
}

本題、mixinsは使ってはいけない

順調に環境もできて、へーほー言いながらtypescriptでreactが書けることに感動しつつ、2章まで読み進めると、mixinの紹介がありました。
当初は「ほほーこれは記述が楽できそうだ」とほくほくして読んでいたのですが、いざtypescriptにて書こうとした際、どうやって書いていいのかわからず、調べていくと、es6ではmixinは廃止になったということ、さらになんと、reactチーム公式のブログでmixinは有害とまで書かれた記事を見つけました。

【↓es6でmixin廃止】
https://reactjs.org/docs/react-without-es6.html#mixins

【↓mixinは有害のブログ】
https://reactjs.org/blog/2016/07/13/mixins-considered-harmful.html

以降は上記ブログの内容に沿ったものになります。

mixinは依存関係の注入に他ならない

ブログで書かれていましたが言われてみればおっしゃる通りです。

  • ある程度の開発の規模になってくると、誰もmixinとして書かれたコードを触りたくなくなるらしい(ましてや新しくチームに参加したメンバーはなおさら)
  • mixinからmixinを呼び出すようなことも可能なため、一つの修正がどこにどう影響するのかわからなくなる
  • きっと古くなって使われていなくなったであろうmixinのコードが仮にあったとしても、どこかで実は使っているのでは、という疑念を完璧に拭い去るのにとんでもない労力を費やすことになる。
  • コンポーネントと違いmixinは名前空間を持たないため識別子同士のコリジョンが起こる。サードパティ製のmixinがあってコンポーネント側のメソッドとコリジョンが発生した場合はコンポーネント側のメソッドを変な名前に変えないといけなくなる。
  • さらにサードパティ側はコリジョンなんて御構い無しにmixinのバージョンアップを行う
  • mixinは最初は簡単なものでも、徐々に雪だるま式にでかくなっていく

便利とか言っていた自分が情けなく思えてくるほどけちょんけちょんに書かれていました。
初学者にとってはグローバル空間に書かれたmixinのようなものをどこからでも呼び出せるのはなんとなく理解しやすくて安心するのですが、大きな陥穽でした。

本で「使った方がいいよ」と書かれているPureRenderMixinさえダメなんですかと思ったらやはりダメなようです。

ブログの内容をかい摘んで訳しつつ、typescriptにおける代替方法を記載していきます。

PureRenderMixin

一番mixinとして使われているであろうPureRenderMixinの以下のようなコードは、

(es5:ブログ引用)
var PureRenderMixin = require('react-addons-pure-render-mixin');
var Button = React.createClass({
  mixins: [PureRenderMixin],
  // ...
});

(es5では)こうしろと

(es5:ブログ引用)
var shallowCompare = require('react-addons-shallow-compare');
var Button = React.createClass({
  shouldComponentUpdate: function(nextProps, nextState) {
    return shallowCompare(this, nextProps, nextState);
  },
  // ...
});

「あーこれは面倒なことになったなあ、たくさんのコンポーネントにshouldComponentUpdate入れなきゃいけないな〜」なんて内心思っていました。

それをtypescriptで書き直しますと、

import * as React from 'react';

interface ButtonProp {
    defaultval: string;
}
interface ButtonState {
    val: string;
}

class Button extends React.PureComponent<ButtonProp, ButtonState> {
    // ...
}

typescriptで(というかes6でも)、React.PureComponentをextendsするだけでPureRenderMixinと同じことができてしまうとのことでした。
たった1年半年ほどで、本に書いてある内容と最新のプログラミングの書き方とでここまで差が出るなんて、恐るべきjavascript…

Reactコンポーネントメソッドのmixinへの書き込み

サードパティが作成したmixinを利用した場合、よくthis.setState({...})等がhandleChange等のmixin側の関数に書かれていることがある(らしい)のですが、こういったのも依存になるのでマイグレーションしていきましょうと。

(es5:ブログ引用)
var SubscriptionMixin = {
  getInitialState: function() {
    return {
      comments: DataSource.getComments()
    };
  },

  componentDidMount: function() {
    DataSource.addChangeListener(this.handleChange);
  },

  componentWillUnmount: function() {
    DataSource.removeChangeListener(this.handleChange);
  },

  handleChange: function() {
    this.setState({
      comments: DataSource.getComments()
    });
  }
};

var CommentList = React.createClass({
  mixins: [SubscriptionMixin],

  render: function() {
    // Reading comments from state managed by mixin.
    var comments = this.state.comments;
    return (
      <div>
        {comments.map(function(comment) {
          return <Comment comment={comment} key={comment.id} />
        })}
      </div>
    )
  }
});

module.exports = CommentList;

確かにこれはきついですね。
setStateがmixin側に書かれていて、 コンポーネント側では素知らぬ顔でthis.state.commentsなんて書いてあったらコード読む側はどこでsetState呼び出されたんだとか、DataSource.getCommentsは何返すんだとかmixinをたどる旅に行かなくちゃいけません。

ではどうするのか、を説明する前に、reactチームのブログに倣ってHigher-Order Componentsについて説明します。

Higher-Order Components

Higher-Order Componentsとは何か?簡単な例をご覧ください。

(es5:ブログ引用)
function addAndLog(x, y) {
  var result = x + y;
  console.log('result:', result);
  return result;
}

function multiplyAndLog(x, y) {
  var result = x * y;
  console.log('result:', result);
  return result;
}

上記の足し算と掛け算を行う関数があったとして、これは次のようにも書けます。

(es5:ブログ引用)
function add(x, y) {
  return x + y;
}

function multiply(x, y) {
  return x * y;
}

function withLogging(wrappedFunction) {
  return function(x, y) {
    var result = wrappedFunction(x, y);
    console.log('result:', result);
    return result;
  };
}

// addAndLog関数の出来上がり:
var addAndLog = withLogging(add);

// multiplyAndLog関数の出来上がり:
var multiplyAndLog = withLogging(multiply);

このwithLogging関数が重要で、上記の場合は関数オブジェクトを受け取って、それをくるんだ関数オブジェクトを返す、いわばそう、ラッパーです。
この辺は自分の古いjavascriptの知識でもついていけてホッとしました。

そして、この考えを応用して、コンポーネントをラップしてコンポーネントを返す抽象度の高いラッパーを作りましょうというのが「Higher-Order Components」の基本的な考え方のようです。

先ほどのSubscriptionMixinが記述されたコードから2つのステップを経てHigher-Order Componentsを作っていきます。

ステップ1

まずcomponentを2つに分けます。

(es5:ブログ引用)
// 下が子コンポーネント.
// propsで受け取ったcommentをrenderする機能のみ
var CommentList = React.createClass({
  render: function() {
    // Note: now reading from props rather than state.
    var comments = this.props.comments;
    return (
      <div>
        {comments.map(function(comment) {
          return <Comment comment={comment} key={comment.id} />
        })}
      </div>
    )
  }
});

// こちらは親コンポーネント.
// DataSourceに書き込んだり<CommentList />をrenderする.
var CommentListWithSubscription = React.createClass({
  getInitialState: function() {
    return {
      comments: DataSource.getComments()
    };
  },

  componentDidMount: function() {
    DataSource.addChangeListener(this.handleChange);
  },

  componentWillUnmount: function() {
    DataSource.removeChangeListener(this.handleChange);
  },

  handleChange: function() {
    this.setState({
      comments: DataSource.getComments()
    });
  },

  render: function() {
    // CommentListのpropsに親コンポーネントのstateを渡す.
    return <CommentList comments={this.state.comments} />;
  }
});

module.exports = CommentListWithSubscription;

mixinになっていた部分が丸っとコンポーネントになりました。

ステップ2

次の最終ステップは親コンポーネントを関数でラップして抽象度を上げ、様々な子コンポーネントから呼び出せるようにしてあげるステップです。
(子コンポーネント側でstateもライフサイクルメソッドも利用しない場合、子側でReact.CreateClass()は必要ないので単純なjsxのみreturnするコンポーネントを親に渡します)

(es5:ブログ引用)
function withSubscription(WrappedComponent) {
  return React.createClass({
    getInitialState: function() {
      return {
        comments: DataSource.getComments()
      };
    },

    componentDidMount: function() {
      DataSource.addChangeListener(this.handleChange);
    },

    componentWillUnmount: function() {
      DataSource.removeChangeListener(this.handleChange);
    },

    handleChange: function() {
      this.setState({
        comments: DataSource.getComments()
      });
    },

    render: function() {
      // スプレッド演算子でpropsとstateを展開.
      return <WrappedComponent {...this.props} {...this.state} />;
    }
  });
}

function CommentList(props) {
  var comments = props.comments;
  return (
    <div>
      {comments.map(function(comment) {
        return <Comment comment={comment} key={comment.id} />
      })}
    </div>
  )
}

module.exports = withSubscription(CommentList);

以上でes5にてマイグレーションが行えました。

これをtypescriptでやるにはいくつか方法がありそうですが、まあまずOOP(オブジェクト指向プログラミング)をかじったことがある人なら誰でも「継承すればできんじゃね?」って考えると思います。

(ちなみにtypescriptによるmixinの公式な代替方法は調べても見つかりませんでした。どなたか情報あれば教えてください…)

mixinを使わず継承にて「Reactビギナーズガイド」の2章にあるTextAreaCounterを書くとこんな感じになりました。

WithSubscription.tsx(親コンポーネント)
import * as React from 'react';

abstract class WithSubscription<P, S> extends React.PureComponent {
    public props: P;
    public state: S;
    componentDidUpdate(oldp: P, olds: S) {
        this._log(self_method_name(), olds);
    }
    _textChange(ev: React.FormEvent<HTMLTextAreaElement>) {
        this.setState({ value: ev.currentTarget.value });
    }
    _log(methodName: string, arg?: {}): void {
        global.console.log(methodName, arg);
    }
}

// 以下は自身の関数名を返すヘルパー関数
function self_method_name() {
    let stack: string | undefined = new Error().stack;
    let caller: string = '';
    if (stack) {
        caller = stack.split('\n')[2].trim();
    }
    return caller;
}
export default WithSubscription;

親コンポーネントはジェネリクスを用いて、propsとstateの型は子コンポーネントから指定できるような抽象クラスにしてあります。
こうすることで、様々なprops、stateを持つコンポーネントから、再利用可能なプログラムとなります。
またabstract(抽象クラス)にすることで継承のために作られたクラスだとパッと見てわかるようになります。
ここではcomponentDidUpdateだけ実装しています。

TextAreaCounter.tsx(子コンポーネント)
import * as React from 'react';
import Counter from './Counter';
import WithSubscription from './WithSubscription';

interface TextareaProp {
    name: string;
}
interface TextareaState {
    value: string;
}
class TextAreaCounter extends WithSubscription<TextareaProp, TextareaState> {
    constructor(props: TextareaProp) {
        super(props);
        this.state = {value: props.name};
        this._textChange = this._textChange.bind(this);
    }
    render(): JSX.Element {
        return (
            <div>
                <textarea {...this.state} onChange={this._textChange}/>
                <h3><Counter value={this.state.value.length}/></h3>
            </div>
        );
    }
}
export default TextAreaCounter;

子コンポーネント側でWithSubscriptionクラスを継承します。
コンストラクタとrenderのみのシンプルなクラスの構造になりました。

Counter.tsx(孫コンポーネント)
import * as React from 'react';

interface CounterProp {
    value: number;
}
class Counter extends React.PureComponent<CounterProp, {}> {
    render(): JSX.Element {
        return (
            <div><span>{this.props.value}</span></div>
        );
    }
}
export default Counter;

孫側では親から渡されたテキストエリアの文字数を表示するだけのコンポーネントが書かれています。

親コンポーネントではジェネリックを用いて子側から色々なタイプのstateやpropsを使えるようにもできました。
一応やりたいことはできたし動くには動くのでまあこれで良いのかな。
あと、複数のクラスを継承したいときなんかは、typescriptは多重継承ができないため、困ってしまいます。
その場合は継承(extends)ではなく実装(impliments)をうまく使って解決できそうなので、また今度。

まとめ

  • mixinはes5でも使わない方がよい。
  • mixinはes6またはtypescriptでは使えない
  • PureRenderMixinを使いたいときはReact.PureComponentを拡張したクラスを実装する
  • es5で書かれたmixinはOOPで言う所の継承を使えばmixinぽく実装できる

mixinをtypescriptでどう実装するのか調べるのに丸1日費やしましたが非常に良い勉強になりました。
typescript、reactともにそのコアな部分に近づけたような気がします。
でもこのペースで読んでいったら、読み終わるまで一体何日かかるのやら…

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.