React+MobX+TypeScriptが個人的にちょうど良い

Vue使いやすい

以前はフロントエンドといえばReact+Reduxでしょ!というのが主流だったが、最近ではVue+Vuex、あるいはNuxt.jsが注目を集めてる。

私自身も、趣味・業務両方でVueを使っていて、とても使いやすいな〜と思ってる。

TypeScript使うとちょっとつらい

TypeScript自体はかなり強力で、例えばkeyofのような機能はとても魅力的だったりする。

ただ、Vueと組み合わせてTypeScriptを使った型付けをしたい場合に、2018年7月現在ではわりと厳しい感じがしている。私が調べた限りでは、Vue&VSCodeを使うことでthisからの補完などはある程度はできるが、Vuexを絡めた場合には頑張って型を書く必要がある。(Nuxtの場合はもっと大変)

このあたりは、ClassComponentを駆使する有志作成のライブラリを使うことである程度はカバーできる。

ただ、人によって好みもあると思うけど、いわゆる一般的に見られるVueの書き方とは大きく離れることになってしまうことが多く、軽い気持ちで導入するにはハードルが高いな〜、と感じてしまう。

Reactの場合

Reactの場合はTypeScriptとの相性が良く、VSCodeを組み合わせた場合もかなり使いやすい。例えば、JSXに記述するpropsも厳密にチェックすることができる。PropTypesを使わずとも事前に検証できるのも心強い。

状態管理は?

王道だとReduxを採用することになると思うが、私はつらい。(これには個人的なトラウマによる偏見もかなり含まれている。)

これはもちろん開発対象のプロダクトやチームとの相性に依る部分もあるけど、Reduxはかなり記述が増えて、重厚なイメージがある。

もっと軽い感じで使いたい。

このあたりはVuexはとても良くできていて、理解もしやすい。

MobX

mobx.js.org

というわけで、ここ最近MobXを使ってみているが、これはいいな〜という手応えを感じている。

クラスベースでの定義

たとえば、Reduxで一通りのフローを作る場合

  • ActionCreator
  • Reducer
  • Store

あたりを書くことになるが、ファイルがめっちゃ増えたり、Reducerがなんかとんでもない感じに成長してしまうみたいなことがあった。Reducerがとにかくやばい。

MobXの場合は、状態管理をしたいクラスからインスタンスを作成して利用する。こんな感じ。

import axios from 'axios';
import { action, observable, toJS } from 'mobx';

export class Item {
  @observable public name: string = '';
  @observable public image: string = '';

  @action.bound
  public async save(): Promise<void> {
    const res = await axios.post('/items', toJS(this));
    return res.data.id;
  }
}

これが非常にわかりやすく、

  • 状態は @observable 定義のインスタンス変数
  • 処理や状態変更は @action.bound のメソッドを実行

だけで、非常にシンプル。実質ただのインスタンス

そしてTypeScriptと組み合わせた場合に嬉しい効果として、クラス定義されているので、別途型定義を書く必要がない。

Reactと組み合わせる場合にはこれが非常に嬉しく、Propsにクラス定義を指定しておくだけで、そこに紐づくプロパティが全部型チェックできる。

これが出来るかどうかということより、「意識せずに簡単にできる」というのが重要で、書いてて幸せな気持ちになれた。

Reactと組み合わせる

Reactと組み合わせる場合は mobx-react を使う。
このあたりの初期化周りは react-redux を使った場合と似てる。

ただ、ストアにするインスタンスを作って突っ込んでるんだな〜っていう感じで、わりとわかりやすい。

import { Provider } from 'mobx-react';
import { Item } from './Item';

const store = {
  item: new Item()
};

const App = () => (
  <Provider {...store}>
    ...
  </Provider>
);

export default App;

参照する場合には@inject を使う。 react-reduxのときの connectに近い。

import { inject, observer } from 'mobx-react';
import * as React from 'react';
import { Item } from './Item';

interface Props {
  item: Item;
}

@inject('item') @observer
export default class ItemContainer extends React.Component<Props> {
  public render() {
    return (
      <React.Fragment>
        <div>{this.props.item.name}</div>
        <ImageComponent item={this.props.item} />
        <button onClick={() => this.props.item.save()}>save</button>
      </React.Fragment>
    );
  }
}

Actionの呼び出しは、直接インスタンス変数のメソッドを呼び出してる。

この部分。

<button onClick={() => this.props.item.save()}>save</button>

これについては賛否両論あるかもしれない。

Reduxを使っていた場合に、子コンポーネントのReduxへの直接依存を避けるためにメソッドもPropsリレーをしてコードを書いたことがあるが、往々にしてバケツリレー地獄になって、最後には辛いなという気持ちになってた。

結局、「ActionをDispatchするのだけは子コンポーネントからでもOK」みたいになることも多くて、それと同じことをやってる感じ。

ただ、TypeScriptを組み合わせることで、どのメソッドがどのコンポーネントで呼ばれているのかを静的チェックすることができるので、これが非常に強力で、Actionがどこで実行されてるのか把握でき、リファクタリングをする際もある程度ビビらずに直していける。

TypeScriptと使う

というわけで、多々TypeScriptと組み合わせた場合のことを書いた通り、TypeScriptとの相性がかなり良いっぽい。

React+MobXだけで書いた場合には、「簡単に状態管理が出来てうれしい!」くらいで終わりそうだが、TypeScriptを組み合わせることで、クラスベースであることが一気に強力な機能に変わるような印象を受けた。

まとめ

というわけで、React+MobX+TypeScriptがかなり良さそうですよっていうのをどこかに書きたくて書いてみた。

やりたいことを「意識せずに」「簡単に」できるのが大事で、そのあたりを今の所ちょうど良い具合で実現してくれるのがこの組み合わせだな〜と感じてる。

良いことばかり書いてるが、趣味で書いている程度なので、大規模プロダクトの場合や、パフォーマンスが要求される場合などはどうなの?とか色々考えれてない部分はある。

けど、書いてて楽しいので、細かいことはいいやってなってる。

同じことを感じている人がいることに期待している。

とはいえ、Vueはやっぱり使いやすい。pugとかに慣れてると、JSXでclassNameって書くたびにアアアアアア!!!!ってなるし、気をつけててもネストが深くなりがちなのはつらい。

そもそもこれを試したのは「Vue+Vuex (or Nuxt) でTypeScriptを使いたいと思ったけど辛かったから」というのが大きいので、逆に言えばそちらが快適に書けるようになれば、そちらを使うことが多くなりそうではある。

ともあれ、何が一番良いかとかではなく、都度状況を見ながら最適なものを選べるようになっていきたいと思った。

終わり。