mobx-react-lite入門 前編: mobx-react-liteのObserver

# 0.はじめに
JavaScriptの「シンプルかつスケーラブルな」状態管理ライブラリこと[MobX](https://github.com/mobxjs/mobx)を[React](https://ja.reactjs.org)と結びつけて、楽しくWebアプリケーションを作れるようになってみたいと思いませんか？

当記事ではReactとMobXを組み合わせて使うためのライブラリ[mobx-react-lite](https://github.com/mobxjs/mobx-react-lite)を使って、**観測可能な状態**と**観測者**による状態管理を俯瞰してみたいと思います。

前編では、mobx-react-liteで提供されるObserverを紹介します。

<details><summary> <font color="#b01">筆者の環境</font> </summary><div>

* **OS:** macOS Mojave 10.14.5
* **ブラウザ:** Safari バージョン12.1.1 
* **[Node.js](https://nodejs.org/ja/):** v10.16.0
* **[Yarn]():** 1.15.2
</div></details>

## 環境と想定読者

[node.js](https://nodejs.org/ja/)のインストールが必要です。node.jsを使うならば、備え付けのパッケージマネージャの`npm`について詳しく知る必要があります。しかし、当記事ではパッケージマネージャとして[Yarn](https://yarnpkg.com/ja/)を用います。yarnは以下からダウンロードできます。

* [インストール|Yarn](https://yarnpkg.com/ja/docs/install)

MobXの前に、軽くReactに関する知識が必要です。

* [Hello World|React](https://ja.reactjs.org/docs/hello-world.html) を確認しておきましょう。JSXを知り、関数型コンポーネントが書けるようになればオッケーです。
* 今回のチュートリアルでは、関数型コンポーネントとHooksをたくさん書くので、以下の大変参考になるQiita記事を目を通しておくかもいいかもしれません。なお、本チュートリアルにおいてはHooksは、都度説明を入れるつもりです。[React 16.8: 正式版となったReact Hooksを今さら総ざらいする|Qiita by uhyo
](https://qiita.com/uhyo/items/246fb1f30acfeb7699da)


# 1.準備

[Next.js 9](https://nextjs.org)を使って、今回のチュートリアルの環境を作っていきましょう。

### パッケージマネージャと依存関係のインストール


作業用ディレクトリを作成し、そこで以下のコマンドを実行することで依存関係（パッケージ）をインストールします。

```terminal:terminal
yarn add next@latest react@latest react-dom@latest mobx mobx-react-lite
```

<details><summary> <font color="#b01">執筆当時のpackage.json</font> </summary><div>

```js:package.json
{
  "dependencies": {
    "mobx": "^5.11.0",
    "mobx-react-lite": "^1.4.1",
    "next": "^9.0.1",
    "react": "^16.8.6",
    "react-dom": "^16.8.6"
  }
}
```
</div></details>

### 開発サーバの立ち上げ

`pages`という名前のディレクトリを作成し、その中に`index.jsx`というファイルを作成します。

```jsx:pages/index.jsx
const Index = () => <p>It Works!</p>;

export default Index;
```

そして、以下のコマンドを実行すると開発サーバが立ち上がります。

```terminal:terminal
yarn next
```

そして、http://localhost:3000 にアクセスした時に以下のように表示されていたら成功です。

![スクリーンショット 2019-07-13 3.11.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/86fdcb9a-0f6b-7393-80d8-52066d033249.png)

開発サーバーはターミナルで`ctr+c`でを打てば終了します。

### Next.jsの基本

`Next.js`は`pages/`配下の`.jsx`ファイルなどで`React`のコンポーネントを`export default`すると、そのディレクトリ名に対応するページが新規作成されます。

また、開発サーバーにおける`Next.js`はファイルの更新を検出し、サーバーを再起動することなく更新を反映させます。

次のチュートリアルの準備のために、`pages/counter.jsx`を作成して中をこのようにします。

```pages/counter.jsx
const CounterPage = () => {
    return (
        <div>
            <p>ここはカウンターページです</p>
            <hr/>
        </div>
    );
};

export default CounterPage;
```

すると先ほどの説明のように、counterというページがブラウザで読み込めるようになります。
http://localhost:3000/counter
![スクリーンショット 2019-07-13 3.24.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/2289162b-b6b6-69db-d05d-a8e2512118be.png)

# 2.Hello MobX

MobXの**観測可能な状態**と**観測者**を早速使ってみましょう。

```pages/counter.jsx
import {Observer, useLocalStore} from "mobx-react-lite"; //追加

const CounterPage = () => {
    /***
     * 観測可能な状態
     * ストア：{counter: number}のように扱える
     */
    const store = useLocalStore(() => ({counter: 0}));

    /***
     * ストアの操作のための関数
     * ボタンに与える
     */
    function increment() {
        store.counter++;
    }

    function decrement() {
        store.counter--;
    }

    return (
        <div>
            <p>ここはカウンターページです</p>
            <hr/>
            {/***
             * 観測者コンポーネント
             * 観測可能な状態の変化に応じて更新される
             */}
            <Observer>{() => (<p>{store.counter}</p>)}</Observer>

            <button onClick={increment}>+</button>
            <button onClick={decrement}>-</button>
        </div>
    );
};

export default CounterPage;
```

http://localhost:3000/counter
![スクリーンショット 2019-07-13 3.53.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/3f00b2fc-cdd0-60f1-4724-4448c71840b2.png)

これで、+を押せばカウントアップされ、-を押せばカウントダウンするページが作れました。

## 解説

#### 観測可能な状態
`mobx-react-lite`では`useLocalStore` Hookを使えば、**観測可能な状態**を作ることができます。**観測可能な状態**の作り方は他にもあります。

```jsx
useLocalStore(() => {return オブジェクト}); // これでオブジェクト型のObservableステートが作れる
```

`useLocalStore`はHookですので、二つのルールがあります。

> フックは JavaScript の関数ですが、2 つの追加のルールがあります。
> 
> * フックは関数のトップレベルのみで呼び出してください。ループや条件分岐やネストした関数の中でフックを呼び出さないでください。
> * フックは React の関数コンポーネントの内部のみで呼び出してください。通常の JavaScript 関数内では呼び出さないでください（ただしフックを呼び出していい場所がもう 1 カ所だけあります — 自分のカスタムフックの中です。これについてはすぐ後で学びます）。
> 
> [フック早わかり|React](https://ja.reactjs.org/docs/hooks-overview.html)


したがって、クラス型のコンポーネントからは`useLocalStore` Hookは使えません。


#### 観測者

`mobx-react-lite`では**観測者**Reactコンポーネントを作ることができます。`Observerコンポーネント`は、子要素のようにReactのコンポーネントを書くことができず、render関数を渡してやる必要があります。つまり`Observerコンポーネント`を使う際は以下の形式になることが多いでしょう。

```jsx
<Observer>{() => (観測可能な状態にアクセスするReact要素)}</Observer>
```

**観測可能な状態**へのアクセスとは、短絡的な話だと`観測可能な状態.観測可能な状態のメンバー`における`.`を含むということです。



# 3. mobx-react-liteで提供される観測者(HOC, Observerコンポーネント, useObserver)

### ちょっと準備：Hooksやコンポーネントを使い回せるようにする

先ほどのチュートリアルで`useLocalStore`によるカウンターのストアを作りました。これを使い回せるようにカスタムHookを作成しましょう。

```hooks/counterStore.js
import { useLocalStore } from "mobx-react-lite";

export function useCounterStore() {
  const store = useLocalStore(() => ({
    counter: 0,
    increment: () => {
      store.counter++;
    },
    decrement: () => {
      store.counter--;
    }
  }));
  return store;
}
```

incrementや、decrementをStoreの中に入れてしまうことで、取り回しが良くなります。

次にボタンもコンポーネントにしましょう。

```components/counterButton.jsx
export const CounterButton = props => (
  <>
    <button onClick={props.store.increment}>+</button>
    <button onClick={props.store.decrement}>-</button>
  </>
);
```

ここまででファイル構成はこのようになっています。

```
.
├── components
│   └── counterButton.jsx
├── hooks
│   └── useCounterStore.js
├── package.json
├── pages
│   ├── counter.jsx
│   └── index.jsx
└── yarn.lock
```

## Observerコンポーネント

Observerコンポーネントは最もよく使う観測者でしょう。使い方はすでに見た通りです。

<details><summary> <font color="#b01">pages/observer.jsxの全体</font> </summary><div>

```pages/observer.jsx
import { Observer } from "mobx-react-lite";

import { useCounterStore } from "../hooks/useCounterStore";
import { CounterButton } from "../components/counterButton";

const Counter = props => <p>{props.counter}</p>;

const CounterPage = () => {
  const store = useCounterStore();

  return (
    <div>
      <p>オブザーバーコンポーネントの例</p>
      <hr />

      <Observer>{() => <Counter counter={store.counter} />}</Observer>
      <CounterButton store={store} />
    </div>
  );
};

export default CounterPage;
```

</div></details> 

Observerコンポーネントでハマるとすればrender propsです。

### Observerコンポーネントが動かない例

Observerコンポーネントは、直下のrender関数の更新をすることができますが、render関数中でさらにrender関数を呼び出された場合、子のrender関数の更新をすることができません。したがって以下のような例ではカウンターが動きません。

#### 動かない例

```jsx
<Observer>
  {() => (
    <Ueshita
      render={props => (
        <>
          ここは{props.name}
          <Counter counter={store.counter} />
        </>
      )}
    >
      {props => (
        <>
          ここは{props.name}
          <Counter counter={store.counter} />
        </>
      )}
    </Ueshita>
  )}
</Observer>
```

#### 動く例

```jsx
<Ueshita
  render={props => (
    <>
      ここは{props.name}
      <Observer>{() => <Counter counter={store.counter} />}</Observer>
    </>
  )}
>
  {props => (
    <>
      ここは{props.name}
      <Observer>{() => <Counter counter={store.counter} />}</Observer>
    </>
  )}
</Ueshita>
```

<details><summary> <font color="#b01">pages/observer2.jsxの全体</font> </summary><div>

```pages/observer2.jsx
import { Observer } from "mobx-react-lite";

import { useCounterStore } from "../hooks/useCounterStore";
import { CounterButton } from "../components/counterButton";

const Counter = props => <p>{props.counter}</p>;

const Ueshita = props => (
  <div>
    {props.render({ name: "上" })}
    <hr />
    {props.children({ name: "下" })}
  </div>
);
const CounterPage = () => {
  const store = useCounterStore();

  return (
    <div>
      <p>オブザーバーHOCの例</p>
      <hr />
      <h2>動かない</h2>
      <Observer>
        {() => (
          <Ueshita
            render={props => (
              <>
                ここは{props.name}
                <Counter counter={store.counter} />
              </>
            )}
          >
            {props => (
              <>
                ここは{props.name}
                <Counter counter={store.counter} />
              </>
            )}
          </Ueshita>
        )}
      </Observer>
      <hr />
      <h2>動く</h2>
      <Ueshita
        render={props => (
          <>
            ここは{props.name}
            <Observer>{() => <Counter counter={store.counter} />}</Observer>
          </>
        )}
      >
        {props => (
          <>
            ここは{props.name}
            <Observer>{() => <Counter counter={store.counter} />}</Observer>
          </>
        )}
      </Ueshita>
      <CounterButton store={store} />
    </div>
  );
};

export default CounterPage;
```

</div></details> 

![スクリーンショット 2019-07-13 21.23.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/3b43950a-d3a6-1e66-6105-d1ec64df3709.png)



**参考:** https://mobx-react.netlify.com/observer-component

## observer HOC

HOCはコンポーネントを引数として、コンポーネントを返す関数です。mobx-react-liteには、ただのコンポーネントを受け取って、それを観測者にする`observer`というHOCがあります。先ほど出てきた`Observer`は先頭が大文字です。注意してください。

```pages/hoc.jsx
import { observer } from "mobx-react-lite";

import { useCounterStore } from "../hooks/useCounterStore";
import { CounterButton } from "../components/counterButton";

const Counter = props => <p>{props.store.counter}</p>;

const HOCCounter = observer(Counter);

const CounterPage = () => {
  const store = useCounterStore();

  return (
    <div>
      <p>オブザーバーHOCの例</p>
      <HOCCounter store={store}/>
      <hr />
      <CounterButton store={store} />
    </div>
  );
};

export default CounterPage;
```

http://localhost:3000/hoc

![スクリーンショット 2019-07-13 19.23.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/d3863da7-f944-8dfb-bc52-e816432b283a.png)

これは、先ほどの例と同じようにカウンターとして機能します。

#### 落とし穴: observerが機能しない
**ちょっと待ってください。** この例のCounterコンポーネントのpropsの取り方が少し冗長なように見えます。この機能を実装するならばいちいちstoreを渡さなくても良さそうに思えますね。つまりこちらの方が汎用性が高いコンポーネントでしょう。

```jsx
const CounterMod = props => <p>{props.counter}</p>;
```

これをobserver HOCに繋ぎます。

```jsx
const HOCCounterMod = observer(CounterMod);
```

そして表示してみましょう。比較用に、Observerコンポーネントに直繋ぎする例も見てみます。

```jsx
<>
カウンターModを直にオブサーバーにつなぐ例
<Observer>{() => <CounterMod counter={store.counter} />}</Observer>
単にHOCで繋いだ例
<HOCCounterMod counter={store.counter}/>
</>
```
<details><summary> <font color="#b01">pages/hoc.jsxの全体</font> </summary><div>

```pages/hoc.jsx
import { observer, Observer } from "mobx-react-lite";

import { useCounterStore } from "../hooks/useCounterStore";
import { CounterButton } from "../components/counterButton";

const Counter = props => <p>{props.store.counter}</p>;

const HOCCounter = observer(Counter);

const CounterMod = props => <p>{props.counter}</p>;

const HOCCounterMod = observer(CounterMod);

const CounterPage = () => {
  const store = useCounterStore();

  return (
    <div>
      <p>オブザーバーHOCの例</p>
      <HOCCounter store={store} />
      <hr />
      カウンターModを直にオブサーバーにつなぐ例
      <Observer>{() => <CounterMod counter={store.counter} />}</Observer>
      単にHOCで繋いだ例
      <HOCCounterMod counter={store.counter}/>
      <CounterButton store={store} />
    </div>
  );
};

export default CounterPage;
```
</div></details>


![スクリーンショット 2019-07-13 19.31.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/007b5435-bfff-1510-668d-47671c0b2f2f.png)

なんと、動かない例が出てしまいました！単にHOCに繋いだものが更新されないのです。

これを動く例にしてみましょう。以下の二つを追加してみます

```jsx
const HOCCounterModFixed = observer(props => <CounterMod counter={props.store.counter}/>);
```

```jsx
<>
StoreからアクセスするようにHOCで繋いだ例
<HOCCounterModFixed store={store}/>
</>
```

<details><summary> <font color="#b01">pages/hoc.jsxの全体</font> </summary><div>

```pages/hoc.jsx
import { observer, Observer } from "mobx-react-lite";

import { useCounterStore } from "../hooks/useCounterStore";
import { CounterButton } from "../components/counterButton";

const Counter = props => <p>{props.store.counter}</p>;

const HOCCounter = observer(Counter);

const CounterMod = props => <p>{props.counter}</p>;

const HOCCounterMod = observer(CounterMod);

const HOCCounterModFixed = observer(props => <CounterMod counter={props.store.counter}/>);


const CounterPage = () => {
  const store = useCounterStore();

  return (
    <div>
      <p>オブザーバーHOCの例</p>
      <HOCCounter store={store} />
      <hr />
      カウンターModを直にオブサーバーにつなぐ例
      <Observer>{() => <CounterMod counter={store.counter} />}</Observer>
      単にHOCで繋いだ例
      <HOCCounterMod counter={store.counter}/>
      StoreからアクセするようにHOCで繋いだ例
      <HOCCounterModFixed store={store}/>
      <CounterButton store={store} />
    </div>
  );
};

export default CounterPage;
```
</div></details> 

![スクリーンショット 2019-07-13 19.35.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/c8be6c3c-aa84-b0bf-07d7-5ceb2129d5cc.png)

これで予想通りに動くようになりました。

#### 落とし穴の理由

MobXにおいて**観測者**が追跡しているものは**観測可能な状態**へのアクセスであり、シンプルな言い方をすれば`観測可能な状態.観測可能な状態のメンバー`における`.`を追っているのです。

```jsx
const CounterMod = props => <p>{props.counter}</p>;
const HOCCounterMod = observer(CounterMod);
```

つまり

```jsx
const HOCCounterMod = observer(props => <p>{props.counter}</p>);
```

を

```jsx      
<HOCCounterMod counter={store.counter}/>
```

と使ったところで、`observer`の引数の中で`store.counter`の`.`が見えていません。

MobXは観測可能な状態のメンバーそのもの、つまり値そのものの変化に対して反応することはできません。

したがって`observer` HOCにおいては、propsで**観測可能な状態**を渡すようにしましょう。観測可能な状態のメンバーをコピーしたものや、観測可能な状態にアクセスした後の値を渡しても、表示は更新されません。

つまりHOCオブジェクトにおいてはJSXの要素の中で`.`があってもダメなのです。

```jsx
<HOCCounterMod counter={store.counter}/>
```

この仕様のため、慣れてくると多くの場合で`Observer`コンポーネントを使うことが一番都合が良いと思うようになってきます。

つまり、以下の内容はちゃんと値の変化に応じて描画されます。

```jsx
<Observer>{() => <HOCCounterMod counter={store.counter}/> />}</Observer>
```

こんなことをするのならば、observer HOCを使った意味がありませんね。

とはいえ、observer HOCを使えばこのようなことができます。

```pages/hoc2.jsx
import { observer } from "mobx-react-lite";

import { useCounterStore } from "../hooks/useCounterStore";
import { CounterButton } from "../components/counterButton";

const Counter = props => <p>{props.counter}</p>;

const CounterPage = observer(() => {
  const store = useCounterStore();

  return (
    <div>
      <p>オブザーバーHOCの例2</p>
      <hr />

      <Counter counter={store.counter} />

      <CounterButton store={store} />
    </div>
  );
});

export default CounterPage;
```

これはちゃんと動きます。しかし再描画の範囲がCounterだけではなく、CounterPage全体ということに注意をしてください。

やたらobserver HOCをディスリましたが、使い用はあるはずです。

でも、もう一つディスりポイントがあって、observer HOCはReactの[Legacy Context](https://ja.reactjs.org/docs/legacy-context.html)に依存しています。その点でもobserver HOCは気味が悪いですね。

**参考:** https://mobx-react.netlify.com/observer-hoc

## useObserver Hook

useObserver Hookは前述の二つの観測者で内部的に利用されているReact Hookです。observer HOCの代わりのような使い方ができます。

実際に現在のObserverコンポーネントの実装(TypeScript)は以下のようになっています。

```tsx:
function ObserverComponent({ children, render }: IObserverProps) {
    const component = children || render
    if (typeof component !== "function") {
        return null
    }
    return useObserver(component)
}
```

useObserverはmobx-react-liteの心臓部と言って差し支えないでしょう。
**参考:** https://github.com/mobxjs/mobx-react-lite/blob/master/src/ObserverComponent.ts

`useObserver`を`observer` HOCのように使いたければ以下のようにしましょう。

```pages/useobserver.jsx
import { useObserver } from "mobx-react-lite";

import { useCounterStore } from "../hooks/useCounterStore";
import { CounterButton } from "../components/counterButton";

const Counter = props => <p>{props.counter}</p>;

const HookCounter = props => {
  //普通はこれをそのままreturnでオッケー：Hookっぽさを出すためにこうした。
  const Component = useObserver(() => (
    <Counter counter={props.store.counter} />
  ));
  return Component;
};

const CounterPage = () => {
  const store = useCounterStore();

  return (
    <div>
      <h2>オブザーバーHOOKの例</h2>
      <HookCounter store={store} />
      <hr />
      <CounterButton store={store} />
    </div>
  );
};

export default CounterPage;
```

# まとめ

* 基本はObserverコンポーネントを利用しましょう。
* 観測者が動かない時は、観測可能な状態からのアクセスを観測できているかチェックする。
 * Render Propsで動かない時は、Observerをrender関数の中に入れる