🧊

React HooksとMobXをあわせて使う

最近はもっぱらWebRTCの世界にどっぷりでしたが、フロントエンドのこともちゃんとやってます!

SkyWayにはショーケースとしてWeb会議アプリがあるんですが、それをこの度リライトしてました。

リライトする理由としては・・・、

  • 当時の設計ままでは改修が面倒なバグが見つかった
  • 単にUIがダサい
  • WebRTCフロントエンドをしばらくやって得たノウハウも反映したい

などなど、まあ理由つけて書き直した感じです。

で、どうせ書き直すならってことで、ReactのHooksも使い込みつつ、MobXと混ぜた場合にどうなるか?ってのを味見しました。

ということを、将来の自分が見返せるようにするためのメモ。

最近のMobXについて

MobXの本体は最新がv5で、これはしばらく変わってないです。
今のところv6の気配もない・・?

Reactバインディングである`mobx-react`は、もうすぐv6が出そうで、まだしばらく出ないって感じです。

ReactのHooksがリリースにあわせて、`mobx-react`でもHooksベースのAPIを提供するための`mobx-react-lite`という別リポジトリができました。
`mobx-react-lite`は、Hooksを味見したりAPIを提供したり議論したりと、次なる`mobx-react`を開発するためのリポジトリ

GitHub - mobxjs/mobx-react-lite: Lightweight React bindings for MobX based on React 16.8 and Hooks

で、`mobx-react`のv6で一部のAPIが`mobx-react-lite`から流用されるらしい。
それだけ先に欲しい場合は、`mobx-react`のRC版を使えばおっけーで、今回は`6.0.0-rc.4`を使いました。

`mobx-react-lite`はいろんな実験的なHooksベースのAPIを提供してます。
ただ、それいる・・?純Hooksでよくない?普通にMobX使えばよくない?みたいなAPIも多いので、個人的には様子見・・って感じです。

`mobx-react`にもあるAPIも、Hooksによって使わなくなるもの、おそらく今後Depricatedされるので使わないほうがよいものなどあるので、あらためて使ってるAPIはこれ!というのも書いておく。

- mobx@5.9.4
  - decorate
  - observable
  - computed
  - action
  - reaction
  - observe
- mobx-react@6.0.0-rc.4
  - Observer

`mobx`のほうは正直v4の頃から外部APIは変わってないので、書くことなし。
いっぱいあるように見えるけど、どれも役割が明確に違うし、どれも必要なのがすぐにわかるはず。

`mobx-react`は、`Provider`コンポーネントと`inject()`という関数でいわゆるコンテキストを代替してたけど、それがHooksによって不要に。
`observer()`で片っ端からコンポーネントをラップしてたのもやめて、必要な箇所でだけ`Observer`コンポーネントを使うように。

というのが個人的なおすすめです。

全体の構造

.
├── main.tsx
├── app.tsx
├── contexts.ts
├── stores
├── effects
├── observers
├── components
└── utils

main / app

`main`はエントリーポイントであり、`App`をマウントするだけ。

`app`もそのままの`App`コンポーネント
`components`や`observers`にあるReactコンポーネントをレイアウト。

Functionalじゃないコンポーネントはコレだけで、ErrorBoundaryを兼ねるため。

contexts

`createContext()`でいわゆるストアをエクスポート。

stores

グローバルに存在する単一のストア。

ドメインごとにストアを分けるのがMobX流で、`ui`とか`media`とかいくつか別れてて、互いには依存しない。

effects

そんなストアを操作するための層。

`useEffect()`や`useCallback()`で利用されるものが全てココにあるし、ここ以外ではストアを更新しない。

なので型は`EffectCallback`かただの関数だけ。

observers

`mobx-react`の`Observer`コンポーネントによって、ストアに更新があった際にRe-renderされるコンポーネント

`useContext()`でストアを取って、`effects`からハンドラを生成して下層にPropsとして配るだけの層。
スタイルに関する記述は一切なし。

根っこのコンポーネントでグローバルな`useEffect()`を呼ぶ。

components

型はすべて`FunctionComponent`で、必要ならHooksを使う。

今まではローカルな状態にもMobXを使ってたけど、`useState()`があるならそっちのほうがいいかなという感じ。
ここに関してはHooks最高!って思った。

MobX自体いらないのでは?論

最初はそう思ったけど、やっぱりまだまだ使うかなーという結論に落ち着いた。

そもそもHooksはclassコンポーネントを代替するためのものいう認識。
そうであるならば、classコンポーネントを使ってた頃に必須だったものが、いきなり不要になるわけはないなーと。

確かにローカルの状態もHooksでシュッと書けるようになったし、`shouldComponentUpdate()`も`memo()`でさくっと済むし、メリットは減ったかもしれん。
それでも全てをローカルコンポーネントに閉じることはできんし、そういう意味でのReact設計論は何一つ変化してないよなーと。

と、うっすら思ってたら、作者も同じような感じだったのを発見した。

このブログでも何回も書いてるけど、やっぱり単一の状態変化から導き出される状態変化を宣言的に書けるのが圧倒的開発UXで、MobXを手放せない個人的な理由かなと思ってる。

Anything that can be derived from the application state, should be derived. Automatically.

まじで額縁にいれて飾りたい感ある。

WebRTC = StateFull

そもそもWebRTCとかやってると、シリアライズできない状態がいっぱいあって、そういうのとミュータブルな思想が相性いいってのもあるかも?

  • 接続用の`RTCPeerConnection`
  • 参加者の数だけ送られてくる`MediaStream(Track)`
  • 自分が送信する`MediaStream(Track)`
  • シグナリング用のチャンネル
  • etc...

そういうわけで、結局はコンテキストなり状態管理の外側との濃厚なお付き合いが避けられないので、そのへん柔軟にやれるライブラリと相性が良いのだなあという感じ。

TypeScriptとの相性

TSで書かれてるMobXなのに、なぜかエラーになるシリーズ。
aka 型付け力が足りないので超えられなかった壁たち。

バグなのか、ミスなのか、仕様なのかわからないので、`@ts-ignore`して運用中のものたち。

再現コードとエラー内容を載せておく。

decorate w/ private accessor

そのクラスに`private`がついてるプロパティやメソッドがあるとエラーになる。

import { decorate, observable } from "mobx";

class Store {
  private _name: string;

  constructor() {
    this._name = "ok";
  }

  get name() {
    return this._name;
  }
}
decorate(Store, {
  // @ts-ignore: to use private accessor
  _name: observable
});

export default Store;

エラーこちら。

Argument of type '{ _name: IObservableFactory & IObservableFactories & { enhancer: IEnhancer<any>; }; }' is not assignable to parameter of type '{ prototype?: MethodDecorator | PropertyDecorator | MethodDecorator[] | PropertyDecorator[] | undefined; }'.
  Object literal may only specify known properties, and '_name' does not exist in type '{ prototype?: MethodDecorator | PropertyDecorator | MethodDecorator[] | PropertyDecorator[] | undefined; }'.

observable.shallow w/ decorate and constructor

`ObservableArray`の型がうまく当たらない。

import { decorate, observable } from "mobx";
import { IObservableArray } from "mobx";

class Store {
  items: IObservableArray<string>;

  constructor() {
    // @ts-ignore: to type IObservableArray
    this.items = [];
  }
}
decorate(Store, {
  items: observable.shallow
});

export default Store;

エラーこちら。

Type 'never[]' is missing the following properties from type 'IObservableArray<string>': spliceWithArray, observe, intercept, clear, and 4 more.

Observer does not accept null

import * as React from "react";
import { FunctionComponent } from "react";
import { Observer } from "mobx-react";

export const Chat: FunctionComponent<{}> = () => {
  return (
    <Observer>
      {() => {
        if (xxx) {
          // return null;
          return <></>;
        }

        return <div />;
      }}
    </Observer>
  );
};

エラーこちら。

Type '() => Element | null' is not assignable to type '() => ReactElement<any, string | ((props: any) => ReactElement<any, string | ... | (new (props: any) => Component<any, any, any>)> | null) | (new (props: any) => Component<any, any, any>)>'.
  Type 'Element | null' is not assignable to type 'ReactElement<any, string | ((props: any) => ReactElement<any, string | ... | (new (props: any) => Component<any, any, any>)> | null) | (new (props: any) => Component<any, any, any>)>'.
    Type 'null' is not assignable to type 'ReactElement<any, string | ((props: any) => ReactElement<any, string | ... | (new (props: any) => Component<any, any, any>)> | null) | (new (props: any) => Component<any, any, any>)>'.

どれもワークアラウンドはあるし、実害はないので良いっちゃ良いけど、やっぱりちょっと嫌。

まとめ

ReactのHooksの最初のとっつきにくいオーラは半端なかったけど、使い慣れてくるとその気持ちもわかってきて、中々に良いですね。

ってもまあそんなにあれこれ駆使するわけではなく、基本的なやつしか使ってないし、使うつもりもないんやけど。

  • `useEffect()`であれこれいっぺんにやらない
  • ちゃんと処理を細分化して複数のEffectにする
  • ちゃんとDisposeできるよう返す

みたいなHooks哲学が他のコードにも活きてくる(というかそうしないと居心地わるい)のは、思わぬ収穫だったかも。