React Class Componentは2度死ぬ （MobXで蘇るClass Componentと2度目の死。Functional Componentをより深く知るために）

# はじめに

ReactのClass Component(Component API)は、時代遅れとなりました。そもそも、classを使ったプログラミングが流行らないのに加え、Class Componentでは、Functional Componentで提供されている便利な機能を使うことができません。時代の流れは、Functional Component (Hook API)だと言っても良いでしょう。

しかし、本当にClass Componentはダメなのでしょうか。Functional Componentで提供されている機能が便利なら、Class Componentに移植すれば良いのではないか？既存の機能ではなく、アーキテクチャの持つ特質という観点から考えて、本当にHook APIが優れているか、そのメリットと限界を検討するべきではないか？・・・そういう観点からClass Componentを振り返ることは、Functional Componentの意義とスコープを理解する上でも重要なステップになるでしょう。

もうすぐ、春のお彼岸。この機会に「一度死んだ」Class Componentを振り返ってみたいと思います。

ちなみに、アーキテクチャそのもの評価にフォーカスするため、以下のような「宗教的」問題には触れませんが、最終的に示すコードは、本質的に、decoratorやclassに依存するものではありません。

* "オブジェクト指向はモダンなパラダイムではないの常に避けるべき" vs. "Componentという「対象(オブジェクト）」に対する操作を整理するのに、オブジェクトの概念はわかりやすいし、可読性を高めるのではないか。"
* "experimentalDecoratorの実装は採用されないことになったので、新Decoratorが実装されるまで使用するべきではない" vs. "Decoratorは、コードの可読性を高めるし、内部の仕様は後から変更すれば良いので、新Decoratorへの準備としても旧Decoratorを使うのは問題ない。"

# 死者の世界(MobX + Class Component)にようこそ

MobXという状態管理ライブラリがあります。これが目指すものは、Hook APIのuseMemoやuseEffectの依存性管理が自動化された世界です。つまり、Class Component + MobXをベースに機能追加していけば、機能面ではHook APIに匹敵することができることになります。

というのも、[mobx-react-class-component](https://www.npmjs.com/package/mobx-react-class-component)を使うことで、以下のようなコードを書くことができます。

```ts
import { component, effect, props, render } from "mobx-react-class-component";
import { observable } from "mobx";

@component
class MyComponent extends React.Component {
  // Hook APIのuseEffectと似ているが、依存性が自動で解決される。
  @effect
  effectOnUpdate() {
    console.log("effect on value updated", this.props.value);
    return () => { console.log("disposed on next update or unmount"); }
  }
  // 依存性がなければ、初回だけ実行され、最後に破棄される
  @effect
  effectOnce() {
    console.log("effect once", untracked(() => this.props.value));
    return () => { console.log("disposed on unmount"); }
  }
  // 特定のDOMが変化したときだけ呼び出すためのテクニック
  ref = observable(React.createRef());
  @effect
  effectRef() {
    console.log("ref", this.ref.current);
    return () => { console.log("disposed on unmount"); }
  }

　// 該当するpropsを深い階層まで比較して同じであったら無視する
  @prop.struct
  routeParams;

  // 該当するpropsをリアクティブにしない
  @prop.static
  repository;

  // 依存関係に変化があったときのみ、再実行される
  @render
  render() {
    return <div>
       <div>this.props.value</div>
       <div>this.props.routeParams</div>
       <div ref={this.ref}>show</div>
    </div>
  }
}
```

これは、Hook APIと比べて、いくつものアドバンテージがあります。

1. 冗長な依存関係の記述が不要になる。また、if文や複雑な階層があっても、追加のコードなしで、メモ化が実現できる。
2. Component APIでは、propの変化が直接renderに影響を及ぼさない場合に、Virtul DOMの生成(renderの呼び出し)をスキップできる。この機能は、管理が肥大化しがちだが、MobXと併用すると、この判定が自動化されるので、生産性の向上とパフォーマンスの向上につながる。
3. 2と関連して、propsが変化したときに、どういう条件で無視するかを、MobXで詳細に設定することができる。

そう言われてみると、Class Componentに戻りたくなりませんか？そうです。Mobx + Class Component。ここは甘い死者の世界なのです。

# ふたたび　Functional Component

ただ、これは、そもそもMobXをReactに導入したことによるメリットである可能性もあります。上のコードで実現されていることを、Functional Component + MobXで実現できるのなら、Class Componentのメリットとは言えなくなってしまいます。そこで、これらの機能をFunctional Componentでどこまで実現できるのか考えてみることにしましょう。

## Functional ComponentでMobXを使う

さて、Funcational ComponentとMobXの組み合わせ自体は、MobX公式の[mobx-react-lite](https://github.com/mobxjs/mobx-react-lite)で比較的簡単に実現できます。まずは、公式のサンプルコードを紹介します。

```js
function Measurement({ unit }) {
    const state = useLocalObservable(() => ({
        unit, // the initial unit
        length: 0,
        get lengthWithUnit() {
            // lengthWithUnit can only depend on observables, hence the above conversion with `useAsObservableSource`
            return this.unit === "inch" ? `${this.length * 2.54} inch` : `${this.length} cm`
        }
    }))
    useEffect(() => {
        // sync the unit from 'props' into the observable 'state'
        state.unit = unit
    }, [unit])

    return <h1>{state.lengthWithUnit}</h1>
}
```

ここで、useLocalObservableの内部の実装を見るとuseStateが使われ、useStateへの引数として初回のみMobXのobservableが生成されています([Source Code](https://github.com/mobxjs/mobx/blob/7d449fb4f65925b39ec10e046e654ec006ffc8ac/packages/mobx-react-lite/src/useLocalObservable.ts))

通常のFunctional Componentと比べると、StoreがMobXに移動し、見通しが良くなりました。以下のようにMobXオブジェクトを分離した書き方の方が意図がはっきりするでしょう。(比較のため、classを使っていますが、使わないように書き換えるのは簡単です）。

```js:Storeの分離
class MeasurementStore {
    constructor() {
        makeAutoObservable(this, { props. observable.shallow });
    }
    props = {};
    length = 0;
    get lengthWithUnit() {
      // lengthWithUnit can only depend on observables, hence the above conversion with `useAsObservableSource`
      return this.props.unit === "inch" ? `${this.length * 2.54} inch` : `${this.length} cm`
    }
}

const Measurement = observer((props) => {
    const store = useState(() => new MeasurementStore()
    Object.assign(store.props, props);
    return <h1>{store.lengthWithUnit}</h1>
})
```

ただ、これには、propsの変更が、直接Virtual DOMに反映しない場合でも、一度はVirtual DOMが生成されてしまうという問題があります。小さな問題だと思う人もいるかもしれませんが、せっかくMobXを使うのですから、このあたりは厳密に行きたいところです。これを解決したい場合は、以下のようなテクニックが使えるでしょう。

```js:Virtual&#x20;DOM生成の抑制
// 共通部分
const component = <T>(target) => {
    const RenderDelegator = ({ rendered } => {
       return rendered;
    })
    return observer((props: T) => {
        const store = useState(() => (new target());
        store.update(props);
        return <RenderDelegator rendered={store.rendered} />
    });
}
// 個別部分
class MeasurementStore {
    constructor() {
        makeAutoObservable(this, { props: observable.shallow });
    }
    props = {};
    length = 0;
    get lengthWithUnit() {
      // lengthWithUnit can only depend on observables, hence the above conversion with `useAsObservableSource`
      return this.props.unit === "inch" ? `${this.length * 2.54} inch` : `${this.length} cm`
    }
    // ここでMobXのcomputedを暗黙的に使っていることに注意
    get rendered() {
        return <h1>{this.lengthWithUnit}</h1>
    }
    update(props) {
       Object.assign(this.props, props);
    }
}
const Measurement = component(MeasurementStore);
```

この場合、&lt;RenderDelegator&gt;のVirtualDOMの生成は、propsの変更の度に発生しますが、propsがVirtual DOMに影響がなければ、MobX化されたstore.renderedが、キャッシュされたDOMを返します。そして、&lt;RenderDelegator&gt;の唯一のpropsであるrenderedの値が等しいため、Virtual DOMの比較は一瞬で終わり、（巨大になるかもしれない)renderedの再生成も、比較も起きないのです。計算量の言葉で言い換えると、本来生成したいVirtual DOMの大きさをnとして、O(1)で済むということです。

さらに、このupdateにuseEffectやuseContextを追加すれば、これらの機能をMobXを通して使うこともできます。

```js
class MeasurementStore {
    constructor() {
        makeAutoObservable(this, { props: observable.shallow, ref: false });
    }
    props = {};
    someContext = null;
    length = 0;
    ref = observable.shallow(React.createRef);
    get lengthWithUnit() {
      // lengthWithUnit can only depend on observables, hence the above conversion with `useAsObservableSource`
      return this.props.unit === "inch" ? `${this.length * 2.54} inch` : `${this.length} cm`
    }
    // ここでMobXのcomputedを暗黙的に使っていることに注意
    get rendered() {
        return <h1>{this.lengthWithUnit}</h1>
    }
    // render後に、lengthWithUnitが変更された時だけ呼ばれる (依存関係の記述不要)
    get valueUpdated() {
        console.log(this.lengthWithUnit);
    }
    update(props) {
       Object.assign(this.props, props);
       this.someContext = useContext(SomeContext);
       useEffect(() => {
          this.valueUpdated;
       })
    }
}
```
これは、先程紹介した、mobx-react-class-componentの挙動とほとんど同じになります。正確に言うと、shouldComponentUpdateがないため、Virtual DOMの比較は完全には避けられず、React内部のコードステップ数は増えますが計算量のオーダーは変わりません。しかも、mobx-react-class-componentが内部でかなりトリッキーなことをしているのに対し、わずか10行でコアな部分が移植できるのは驚異的です 。

# まとめ

レガシーとみなされがちなClass Componentですが、MobXと組み合わせることで、Functional Componentの欠点を解消したパワフルなフレームワークを作ることができます。できますが、これは一面的な見方です。MobX + Functional Componentにいくつかのテクニックを組み合わせると、より簡潔に同じことを表現できるからです。

これで、お亡くなりになったClass Componentを一時的に蘇らせるイベントは終わりです。MobX + Class Componentは、再び私たちの元から去っていきました。でも、MobX + Class Componentに似た何かが身近にいる・・・つまり、私たちはいつでもMobX + Functional Componentを使えるということに気づいてもらえると幸いです。そうすれば、きっと亡くなったClass Componentも喜ぶに違いありません。

死者と再会する温かい映画を見たいという方は、こちらがおすすめ。

* [リメンバー・ミー(Disney)](https://www.disney.co.jp/movie/remember-me.html)

# 参考情報

MobX公式はこちら

* [MobX](https://mobx.js.org/README.html)

MobXを使った非同期の処理を簡単にしてくれるライブラリ。かなり便利です。

* [mobx-async-computed](https://www.npmjs.com/package/mobx-async-computed)

本文で死んでもらった、MobX + React Class Componentのbinding。話の流れで死んでもらいましたが、ライブラリとしてはこれからも使えます。

* [mobx-react-class-component](https://www.npmjs.com/package/mobx-react-class-component)