なる早でTypescript + Next + MobX + mobx-react-lite でグリグリ動く「買い物リスト」みたいなのを作る

# はじめに
この記事では[Typescript](https://www.typescriptlang.org)(.tsx)で[Next.js@canary](https://nextjs.org)のサーバーを立て、[MobX](https://github.com/mobxjs/mobx)の動くものをちょいと作ります。

- これはとりあえず動くものを素早く作るためのチュートリアルです
- Typescriptを手っ取り早く設定するために、執筆時点ではNext.jsの<ruby>canary<rt>カナリヤ</rt></ruby>バージョンを使います


#### 想定読者
以下のいずれか

* TypescriptとReactが読める
* これを読んだ人：[なる早でTypescriptで（勉強用の）Next.jsサーバーを立ち上げる
](https://qiita.com/NanimonoDemonai/items/e79f7b72a3023248cf5a)

<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>

#### 今日作るもの
![画面収録 2019-06-30 22.26.11.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/e45af08a-355e-d902-5cca-5c08c6fedec5.gif)

**できたやつ:** https://github.com/NanimonoDemonai/okaimono/tree/master
# プロジェクト準備

#### パッケージインストール
```
yarn add react react-dom next@canary mobx mobx-react-lite uuid
yarn add -D typescript @types/react @types/react-dom @types/node @types/uuid babel-preset-mobx
```
<font color="#bbb">*/*TODO: Next.js 8.1.1がリリースされたら＠canaryを消す***/*</font>

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

```json:package.json
{
  "dependencies": {
    "mobx": "^5.10.1",
    "mobx-react-lite": "^1.4.1",
    "next": "^8.1.1-canary.63",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "uuid": "^3.3.2"
  },
  "devDependencies": {
    "@types/node": "^12.0.10",
    "@types/react": "^16.8.22",
    "@types/react-dom": "^16.8.4",
    "@types/uuid": "^3.4.4",
    "babel-preset-mobx": "^2.0.0",
    "typescript": "^3.5.2"
  }
}
```

</div></details>
#### 各種configの作成
以下のファイルを```package.json```と同階層に作成してください。

**.babelrc**

あとあと、クラスデコレータを使うので".babelrc"を作成して、編集しておく、`preset:"next/babel"`は`next@canary`に付いてくる。

```json:.babelrc
{
  "presets": [
    "next/babel",
    "mobx"
  ]
}
```

**indexページの作成**

`pages/index.tsx`を作成する。

```tsx:pages/index.tsx
export default () => (
   <h1>It Works!</h1>
);
```

**tsconfig.json**
```  "compilerOptions": {}```のメンバに```"experimentalDecorators": true,```があれば良い。

`tsconfig.json`を作成せずに、`yarn next`で開発サーバーを起動することで作成されるデフォルトの`tsconfig.json`にこのオプションを挿すか、`tsconfig.json`ファイルを作りNext.jsの[GitHub](https://github.com/zeit/next.js#typescript)に書かれている`tsconfig.json`にこのオプションを挿す。

```js:tsconfig.json
{
  "compilerOptions": {
    "allowJs": true, /* Allow JavaScript files to be type checked. */
    "alwaysStrict": true, /* Parse in strict mode. */
    "esModuleInterop": true, /* matches compilation setting */
    "isolatedModules": true, /* to match webpack loader */
    "jsx": "preserve", /* Preserves jsx outside of Next.js. */
    "lib": ["dom", "es2017"], /* List of library files to be included in the type checking. */
    "module": "esnext", /* Specifies the type of module to type check. */
    "moduleResolution": "node", /* Determine how modules get resolved. */
    "noEmit": true, /* Do not emit outputs. Makes sure tsc only does type checking. */

    "experimentalDecorators": true, /* ここに挿した */

    /* Strict Type-Checking Options, optional, but recommended. */
    "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
    "noUnusedLocals": true, /* Report errors on unused locals. */
    "noUnusedParameters": true, /* Report errors on unused parameters. */
    "strict": true /* Enable all strict type-checking options. */,
    "target": "esnext" /* The type checking input. */
  }
}
```

<details><summary> <font color="#b01">執筆当時のデフォルトの`tsconfig.json`</font> </summary><div>

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "exclude": [
    "node_modules"
  ],
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx"
  ]
}
```

これに```"experimentalDecorators": true,```を挿すとこうなる

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "experimentalDecorators": true
  },
  "exclude": [
    "node_modules"
  ],
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx"
  ]
}
```
</div></details>

**開発用サーバの起動**

以下のコマンドを実行すると

```
yarn next
```

以下にサーバが立つ
http://localhost:3000


# 品物のViewとStoreを作る
まずはこれを作りましょう
![ww.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/b568aa89-05f2-3429-e71a-bed99bdec555.gif)

### storeを作る
まずは、データを取り扱うstoreから手をつけます。```store/Item.ts```を新規作成しましょう。

IDEで補完をよしなにしてもらうためにまずは型から書きます。

```tsx:store/Item.ts
export interface ItemData {
    readonly name: string;  //商品名
    readonly price: number; //値段
}

export interface ItemModel extends ItemData {
    readonly uuid: string;  //map用のuuid
    readonly count: number; //個数
    readonly fullPrice: number;//合計
    readonly decrementable: boolean;//マイナスボタンが押せるか？
}

export const defaultItemData: ItemData = {
    name: "ダミー",
    price: 50
};
```

そして実際に操作するclassを書いていきましょう。

```tsx:store/Item.ts
import {action, computed, observable, configure} from "mobx";
import uuid from "uuid";

configure({enforceActions: "observed"});

//中略

export class ItemController implements ItemModel {
    readonly name: string;
    readonly price: number;
    readonly uuid: string;
    @observable private _count: number;


    constructor(data?: Partial<ItemData>) {
        const initializer: ItemData = {...defaultItemData, ...data}
        this.name = initializer.name;
        this.price = initializer.price;

        //uuid作成
        this.uuid = uuid.v4();

        this._count = 0;
    }

    @computed
    get count(): number {
        return this._count;
    }

    @computed
    get fullPrice(): number {
        return this._count * this.price;
    }

    @computed
    get decrementable(): boolean {
        return this._count > 0;
    }

    @action.bound
    increment() {
        this._count++;
    }

    @action.bound
    decrement() {
        if (this.decrementable)
            this._count--;
    }
}
```

上から順に説明します、
`configure({enforceActions: "observed"});`は`observable`な値を`action`の外で変更できないようにするための厳し目モードにするための設定です。

このクラスは`@observable`な値である`_count`を操作するためのクラスです。

`@computed`は`_count`によって計算される値であり、`_count`の変化に応じて、値が更新されます。`@action.bound`は`@observable`な値を更新する関数です。`@action`だと`this`が`bind`されないので面白くないです。

### storeをとりあえず使ってみる

```tsx:pages/itemTest
import {ItemController} from "../store/Item";
import {Observer} from "mobx-react-lite";

const itemController1 = new ItemController();

const itemController2 = new ItemController({
    name: "金塊",
    price: 50000
});


export default () => (
    <>
        <div>
            <p>品名:{itemController1.name}</p>
            <p>値段:{itemController1.price}</p>
            <p>UUID:{itemController1.uuid}</p>
            <p>個数:{itemController1.count}</p>
            <p>総額:{itemController1.fullPrice}</p>
            <button
                onClick={()=>{
                    itemController1.increment();
                }}
            >
                +
            </button>
            <button
                onClick={()=>{
                    itemController1.decrement();
                }}
                disabled={!itemController1.decrementable}
            >
                -
            </button>
        </div>

        <div>
            <p>品名:{itemController1.name}</p>
            <p>値段:{itemController1.price}</p>
            <p>UUID:{itemController1.uuid}</p>
            <Observer>{() =>
                <p>個数:{itemController1.count}</p>
            }</Observer>
            <Observer>{() =>
                <p>総額:{itemController1.fullPrice}</p>
            }</Observer>
            <button
                onClick={()=>{
                    itemController1.increment();
                }}
            >
                +
            </button>
            <Observer>{() =>
                <button
                    onClick={() => {
                        itemController1.decrement();
                    }}
                    disabled={!itemController1.decrementable}
                >
                    -
                </button>
            }</Observer>
        </div>

        <div>
            <p>品名:{itemController2.name}</p>
            <p>値段:{itemController2.price}</p>
            <p>UUID:{itemController2.uuid}</p>
            <Observer>{() =>
                <p>個数:{itemController2.count}</p>
            }</Observer>
            <Observer>{() =>
                <p>総額:{itemController2.fullPrice}</p>
            }</Observer>
            <button
                onClick={()=>{
                    itemController2.increment();
                }}
            >
                +
            </button>
            <Observer>{() =>
                <button
                    onClick={() => {
                        itemController2.decrement();
                    }}
                    disabled={!itemController2.decrementable}
                >
                    -
                </button>
            }</Observer>
        </div>
    </>
)
```
http://localhost:3000/itemTest
![画面収録 2019-07-01 0.30.33.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/f874f076-9203-0e1f-b04d-0664cfa8d48d.gif)


これがMobXとmobx-react-liteの威力です。`<Observer>{() =>`~`}</Observer>`で囲った要素は`@observable`および、`@computed`な値の変化があれば適切に更新されます。

一通り動きがわかったところで、見て呉れを良くしましょう。

### viewを作る

```tsx:component/Item.tsx
import {FC} from "react";
import {ItemController, ItemData} from "../store/Item";
import {Observer} from "mobx-react-lite";

export const ItemDescription: FC<ItemData> = props => (
    <div>
        <p>商品名:{props.name}</p>
        <p>価格:{props.price}</p>
    </div>
);

export interface ItemCountProps {
    count: number;
    fullPrice: number;
}

export const ItemCount: FC<ItemCountProps> = props => (
    <div>
        <span className={"count"}>個数:{props.count}</span>
        <span>合計金額:<b>{props.fullPrice}</b></span>

        { /*language=CSS*/}
        <style jsx>{`
            .count {
                padding-right: 2em;
            }
        `}</style>
    </div>
);

export const Item: FC<{ controller: ItemController; }> = props => (
    <div className={"item"}>
        <ItemDescription name={props.controller.name} price={props.controller.price}/>
        <hr/>
        <Observer>{() =>
            <ItemCount count={props.controller.count} fullPrice={props.controller.fullPrice}/>
        }</Observer>
        <button onClick={
            () => {
                props.controller.increment()
            }
        }>
            +
        </button>
        <Observer>{() =>
            <button
                onClick={
                    () => {
                        props.controller.decrement()
                    }
                }
                disabled={!props.controller.decrementable}
            >
                -
            </button>
        }</Observer>
        {/* language=CSS*/}
        <style jsx>{`
            .item {
                border: double;
                padding: 1em;
                margin: 1em;
            }
        `}</style>
    </div>
);
```

```tsx:pages/itemViewTest
import {ItemController} from "../store/Item";
import {Item} from "../components/Item";

const itemController = new ItemController({
    name: "金塊",
    price: 50000
});


export default () => (
    <>
        <Item controller={itemController}/>
    </>
)
```
http://localhost:3000/itemViewTest
![スクリーンショット 2019-07-01 0.48.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/138c662d-4052-9e2d-22ff-47e025c6aea2.png)

# 品物のリストを作る

### storeを作る

```store/ItemList.ts
import {ItemController, ItemData} from "./Item";
import {action, computed, configure, observable, IObservableArray} from "mobx";

configure({enforceActions: "observed"});

export interface ItemListModel {
    readonly items: ReadonlyArray<ItemController>;
    readonly fullPrice: number;
}

export class ItemListController implements ItemListModel {
    private readonly _items: IObservableArray<ItemController>;

    constructor(items?: ItemController[] = []) {
        this._items = observable(items);
    }

    @computed get items(): ReadonlyArray<ItemController> {
        return this._items;
    }

    @computed get fullPrice() {
        return this._items.reduce((acc, cur) => acc + cur.fullPrice, 0)
    }

    @action.bound
    addItem(data: Partial<ItemData>) {
        this._items.push(new ItemController(data));
    }

    @action.bound
    removeChildren(child: ItemController) {
        this._items.remove(child);
    }
}
```

`IObservableArray`は`observable(array)`で作ることができる、observableな配列です。名前の通りですね。`IObservableArray`はそれ自体がobservableなので、`@observable`をつけなくても動きます。なお、つけても動きます。
**参考:** [MobX公式ドキュメント:Array](https://mobx.js.org/refguide/array.html)

この`IObservableArray`は通常の`Array`に加えていくつかの関数が生えており、特段に便利なのは、`remove(value)`です。これは`IObservableArray`の子要素を渡せば、その子要素を消す関数です。

### とりあえずstoreを使ってみる

```tsx:itemListTest.tsx
import {ItemListController} from "../store/ItemList";
import {Observer} from "mobx-react-lite";
import {Item} from "../components/Item";

const list = new ItemListController();
list.addItem({
    name: "金塊",
    price: 50000
});
list.addItem({
    name: "蟹",
    price: 300000
});
let dummy = 0;

export default () => (
    <>
        <Observer>{() =>
            <>
                {list.items.map(e => <Item controller={e} key={e.uuid}/>)}
            </>
        }</Observer>
        <Observer>{() =>
            <p>{list.fullPrice}</p>
        }</Observer>
        <button
            onClick={()=>{
                list.addItem({
                    name: `ダミーくん${dummy}号`,
                    price: 3
                });
                dummy++;
            }}
        >
            ダミーを増やす
        </button>
    </>
)
```
![2qvh4-81l0o.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/cc10b501-63e2-77fa-8043-d46bd50c20c9.gif)

ほぼほぼ完成ですが、一応これもコンポーネントにしましょう。

### viewを作る
```tsx:components/ItemList.tsx
import {Observer} from "mobx-react-lite";
import {ItemListController} from "../store/ItemList";
import {Item} from "./Item";
import {createContext, FC} from "react";

export const ItemListControllerContext = createContext<ItemListController | null>(null);

export const ItemList: FC<{ controller: ItemListController; }> = props => (
    <div>
        <ItemListControllerContext.Provider value={props.controller}>
            <Observer>{() =>
                <div>
                    {props.controller.items.map(e => <Item controller={e} key={e.name}/>)}
                </div>
            }</Observer>

            <hr/>

            <p>合計金額総和:
                <Observer>{() =>
                    <span className={"goukei"}>{props.controller.fullPrice}</span>
                }</Observer>
            </p>
            {/* language=CSS*/}
            <style jsx>{`
                .goukei {
                    padding-left: 1em;
                    color: red;
                }
            `}</style>
        </ItemListControllerContext.Provider>
    </div>
);
```

工夫した点は、React純正の`Provider`を使って、`ItemList`以下のモジュールで`ItemListController`を使えるようにしたことです。これを使ってみましょう。
**参考:** [useContextのしくみ - Qiita](https://qiita.com/ossan-engineer/items/8f2640e4ee053b353943)
### ProviderとuseContextを使った、Removerを作ってみる

```tsx:component/Remover.tsx
import {FC, useContext} from "react";
import {ItemController} from "../store/Item";
import {ItemListControllerContext} from "./ItemList";

export const Remover: FC<{ controller: ItemController; }> = props => {
    const list = useContext(ItemListControllerContext);
    return (
        <>
            {list != null &&
            <button onClick={() => {
                list.removeChildren(props.controller)
            }}>
                削除
            </button>
            }
        </>
    )
};
```

`Provider`と`useContext`を使って削除ボタンを作ることができました。これを、`Item.tsx`に挿しておきましょう。また、これはProvideがないときには表示されません。

**参考:** [条件付きレンダー - React #論理 && 演算子によるインライン If
](https://ja.reactjs.org/docs/conditional-rendering.html#inline-if-with-logical--operator)

```tsx:Item.tsx
import {FC} from "react";
import {ItemController, ItemData} from "../store/Item";
import {Observer} from "mobx-react-lite";
import {Remover} from "./Remover"; //追加

//中略

export const Item: FC<{ controller: ItemController; }> = props => (
    <div className={"item"}>
        <ItemDescription name={props.controller.name} price={props.controller.price}/>
        <hr/>
        <Observer>{() =>
            <ItemCount count={props.controller.count} fullPrice={props.controller.fullPrice}/>
        }</Observer>
        <button onClick={
            () => {
                props.controller.increment()
            }
        }>
            +
        </button>
        <Observer>{() =>
            <button
                onClick={
                    () => {
                        props.controller.decrement()
                    }
                }
                disabled={!props.controller.decrementable}
            >
                -
            </button>
        }</Observer>
        <Remover controller={props.controller}/> {/* ここに挿した*/}
        {/* language=CSS*/}
        <style jsx>{`
            .item {
                border: double;
                padding: 1em;
                margin: 1em;
            }
        `}</style>
    </div>
);
```

動作確認用に適当に使ってみます。

```tsx:pages/itemListViewTest.tsx
import {ItemListController} from "../store/ItemList";
import {ItemList} from "../components/ItemList";

const list = new ItemListController();
list.addItem({
    name: "金塊",
    price: 50000
});
list.addItem({
    name: "蟹",
    price: 300000
});

export default () => (
    <ItemList controller={list}/>
);
```
http://localhost:3000/itemListViewTest
![c80e2-ypyoj.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/0375a410-4bdb-dd7f-fdf3-d4e1109abf0f.gif)


# 品物追加コンポーネントを作る
これでラストです。

### storeを作る
```tsx:store/ItemAdder.ts
import {action, computed, observable,configure} from "mobx";
import {ItemListController} from "./ItemList";

configure({enforceActions: "observed"});

export interface ItemAdderModel {
    readonly name: string;
    readonly price: string;

}

export class ItemAdderController implements ItemAdderModel{
    @observable private _name: string;
    @observable private _price: string;
    private _list: ItemListController;

    constructor(list: ItemListController) {
        this._name = "";
        this._price = "0";
        this._list = list;
    }

    @computed get name(){
        return this._name;
    }
    @computed get price(){
        return this._price;
    }

    @computed get isNumberError(): boolean {
        return this._price == "" || isNaN(Number(this._price));
    }

    @computed get isAddable(): boolean {
        return !this.isNumberError && this._name.length != 0;
    }

    @action.bound
    onNameChange(text: string) {
        this._name = text;
    }

    @action.bound
    onNumberChange(text: string) {
        this._price = text;
    }

    @action.bound
    onAdd() {
        if (this.isAddable) {
            this._list.addItem({
                name: this._name,
                price: Number(this._price)
            });

            this._name = "";
            this._price = "0";
        }

    }


}
```

### viewを作る
```tsx:components/ItemAdder.tsx
import {FC} from "react";
import {Observer} from "mobx-react-lite";
import {ItemAdderController} from "../store/ItemAdder";


export const ItemAdder: FC<{ controller: ItemAdderController }> = props => (
    <>
        <Observer>{() =>
            <p>商品名:<input type="text"
                          onChange={
                              event1 => {
                                  props.controller.onNameChange(event1.target.value)
                              }
                          }
                          value={props.controller.name}
            /></p>
        }</Observer>
        <Observer>{() =>
            <p>値段:<input type="number"
                         onChange={
                             event1 => {
                                 props.controller.onNumberChange(event1.target.value)
                             }
                         }
                         value={props.controller.price}
            />
                {props.controller.isNumberError &&
                <span style={{color: "red"}}>入力エラー(数値を入力してください)</span>
                }
            </p>
        }</Observer>
        <Observer>{() =>
            <button disabled={!props.controller.isAddable}
                    onClick={props.controller.onAdd}
            >
                作成
            </button>
        }</Observer>
    </>
);
```
ポイントは、valueもonChangeも全部MobX任せにすることです。

# 完成させる

indexに今まで作ったものをまとめましょう。

```tsx:pages/index.tsx
import {ItemListController} from "../store/ItemList";
import {ItemList} from "../components/ItemList";
import {ItemAdderController} from "../store/ItemAdder";
import {ItemAdder} from "../components/ItemAdder";

const list = new ItemListController();
list.addItem({
    name: "金塊",
    price: 50000
});
list.addItem({
    name: "蟹",
    price: 300000
});

const adder = new ItemAdderController(list);

export default () => (
    <>
        <ItemAdder controller={adder}/>
        <ItemList controller={list}/>
    </>
);
```

http://localhost:3000

これで冒頭に作ったものが動いているはずです。

# まとめ

* MobXとmobx-react-liteを使えばチャチャっとお買い物リストみたいなアプリを作れる
* MobXのデコレータを使えるようにするには、少し設定がいる
* `@observable`や`@computed`の変化は`<Observer>{() =>`~`}</Observer>`で囲めばよしなに更新される
* `configure({enforceActions: "observed"});`を入れておくと、`@observable`の変更を`@action`を通さずにすると怒られが発生して嬉しい

[続き見たいみたいなのを書きました](https://qiita.com/NanimonoDemonai/items/b47723d5ac73bbad2d76)