かんたん自販機で考える React のロジックいろは #1 状態・ステート・写像

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234029/562eaeff-4017-7b8f-6548-408e8f1e4716.jpeg)

## お題

一種類の缶入りジュース(代金3ゴールド)が売ってある自販機を考えてみましょう。

- 投入できるコインは、1ゴールド硬貨の一種類のみ、
- 投入したコインが3枚以上のときに「購入」ボタンを押すと缶が出てきます。
  - 代金3ゴールドを差し引いて、残りのコインはお釣りとして出てきます。
- 投入したコインが3枚になる前に「返却」ボタンを押すと、投入したコインが出てきます。

この仕様を、

- コンポーネントの持つ状態
- ユーザーイベントによって実行される処理

に着目して React のコンポーネントに落とし込んでみましょう。

```tsx
"use client";

import { FC, useState } from "react";

export const VendingApp: FC = () => {
  // 1. ステート

  // 一時的にコインを保持するストレージ
  // (購入したあとに飲み込まれたコインは省略)
  const [coins, setCoins] = useState<number>(0);

  // 3. ユーザーイベント

  // コインを挿入
  const insertCoin = () => {
    setCoins(coins + 1);
  };

  // 購入
  const buy = () => {
    if (coins < 3) {
      return;
    }
    const prevCoins = coins
    setCoins(0);
    window.alert("缶が出てきました！");
    window.alert(`${prevCoins - 3}枚のお釣りが出てきました！`);
    // 遠回りなコードに見えますが、 
    // stale closure のことを考えなくて良いようにわざとです。
  };

  // お釣りを取り出す
  const receiveChange = () => {
    const prevCoins = coins;
    setCoins(0);
    window.alert(`${prevCoins}枚のお釣りが返ってきました！`);
  };

  // 4. JSX で UI を組み立てる
  // 関数は第一級オブジェクトなので、 onClick={insertCoin} のように書いても問題ありません。
  return (
    <div>
      <div>コイン: {coins}枚</div>
      <div>
        <button onClick={() => insertCoin()}>コインを入れる</button>
        <button onClick={() => buy()}>購入する</button>
        <button onClick={() => receiveChange()}>コインを返却</button>
      </div>
    </div>
  );
};
```


## 購入ボタンを非活性化する条件を追加

ちょっとした仕様を追加してみましょう。

コインが3枚未満のときに「購入する」ボタンが押せるのはおかしいので、そのような条件では「購入できない」として、ボタンを非活性化(disable)しましょう。

```tsx
// 1. ステート
const [coins, setCoins] = useState<number>(0);

// 購入可能かどうかの状態
const [canBuy, setCanBuy] = useState<boolean>(false);

useEffect(() => {
  setCanBuy(coins >= 3);
}, [coins]);

// 中略
<button disabled={!canBuy} onClick={() => buy()}>
  購入する
</button>
```

...

ちょっと待って下さい、このコードは

- 初期値は false、それ以降は「coins >= 3」かどうかで条件判定をしている
  - 暗黙的に、同じ知識を二箇所に書いている (DRY 原則違反)
- 「setCanBuy が他の要因で更新されることがない」ことを保証できない

という問題があります。

頭を一度クリアして、もっとシンプルに、こう考えてみてはいかがでしょうか？

> canBuy とは、**つねに** coins >= 3 であるときに真であり、そうでないときは偽である

実をいうと、React はそのような仕様を素直にコードに落とし込むことができるパラダイムになっていて、さきほど挙げた useEffect を使ったコードの問題点をクリアしています。


```diff_tsx
  // 1. ステート

  const [coins, setCoins] = useState<number>(0);

+ // 2. 写像
+
+ // 購入可能かどうか
+ const canBuy = coins >= 3;

- // 購入可能かどうかの状態
- const [canBuy, setCanBuy] = useState<boolean>(false);
-
- useEffect(() => {
-   setCanBuy(coins >= 3);
- }, [coins]);
```

もちろん、以下のようなケースであれば、2つのステートと useEffect で実装するべきですが、

- 独自に初期状態を持つ
- ほかの要因によって状態が変わることがある
- 非同期である (`use()` が使えるようになれば useEffect が不要になります)

今回の場合に限ってはステートを追加しないコードのほうが、的確に仕様を噛み砕いて React のコードに落とし込めた解釈である、と言えるでしょう。

まるで、スクリーン上の影絵が、手の動きに合わせて形をかえるのと同じような関係なので、仮に「**写像**」と呼ぶことにします。

:::note
この「**つねに**」は、数学や物理で言うところの

- $y$ は $x$ の関数であり $y = 2x + 3$ と表せる。
- 数学における恒等式
- 物理における方程式 (例: $m\vec{a}=\vec{F}$)

に似ていると思います。
:::

### 補足「なぜこんな書き方ができるの？」と思った人へ

```tsx
const SomeComponent: FC = () => {
  // 関数の中身
}
```

なぜコンポーネントの中に書いた式が「つねに」という意味になるのかというと、 React が**再レンダリングのたびに「関数の中身」を全て、上から下まで順番に実行** しているからです。

手前味噌ですが、この React 関数コンポーネントのアーキテクチャについて詳しめに解説したので、気になったら読んでみてください。

https://qiita.com/honey32/items/58e56e407d4d87e294a4

## まとめ

このようにして、素朴に「状態」だと思われていたものも、

- ステート
  - 独立した状態
  - 初期値が決まっている
  - set〇〇 したとき以外は値が再計算されず、再利用される
- 写像
  - 他のステートや写像に（即座に）連動する
  - 独自に初期値を持ったり、独自に値が変化することがない

に分けることが出来ます。少し話は飛躍しますが、

- 状態
  - 真の状態（ステート）
  - 従属的な状態（写像）
- ユーザーイベント
- 外部システムとの同期（useEffect）

これらをキッチリと意識しながら仕様を噛み砕くことで、コードを簡潔に保つことができ、（誤差程度でしょうが）画面のチラツキや無駄な再レンダリングを防げます。

何よりも嬉しいのは「仕様が変更されたけど、どこから直せばいいか分からない💦」「この式を修正したらどこかで予想外の挙動をするかもしれない...」という事態を避けることにも繋がる点です。

### 参考記事

React 公式のドキュメント。 useEffect を使わなくて良いケースについてもっと知りたければ、こちらも読んでみてください。

というか React は基礎さえ押さえれば簡単なので、公式ドキュメントを読みこむべき。

https://ja.react.dev/learn/you-might-not-need-an-effect


## コード全文


```tsx
"use client";

import { FC, useState } from "react";

export const VendingApp: FC = () => {
  // 1. 状態

  // 一時的にコインを保持するストレージ
  // (購入したあとに飲み込まれたコインは省略)
  const [coins, setCoins] = useState<number>(0);

  // 2. 写像

  // 購入できるかどうか
  const canBuy = coins >= 3;

  // 3. ユーザーイベント

  // コインを挿入
  const insertCoin = () => {
    setCoins(coins + 1);
  };

  // 購入
  const buy = () => {
    if (coins < 3) {
      return;
    }
    const prevCoins = coins;
    setCoins(0);
    window.alert("缶が出てきました！");
    window.alert(`${prevCoins - 3}枚のお釣りが出てきました！`);
  };

  // お釣りを取り出す
  const receiveChange = () => {
    const prevCoins = coins;
    setCoins(0);
    window.alert(`${prevCoins}枚のコインが出てきました！`);
  };

  // 4. JSX で UI を組み立てる

  return (
    <div>
      <div>コイン: {coins}枚</div>
      <div>
        <button onClick={() => insertCoin()}>コインを入れる</button>
        <button disabled={!canBuy} onClick={() => buy()}>
          購入する
        </button>
        <button onClick={() => receiveChange()}>コインを返却</button>
      </div>
    </div>
  );
};
```