6

投稿日

更新日

Organization

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

お題

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

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

この仕様を、

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

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

"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)しましょう。

// 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 を使ったコードの問題点をクリアしています。

  // 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 のコードに落とし込めた解釈である、と言えるでしょう。

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

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

  • yx の関数であり y=2x+3 と表せる。
  • 数学における恒等式
  • 物理における方程式 (例: ma=F)

に似ていると思います。

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

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

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

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

まとめ

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

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

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

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

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

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

参考記事

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

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

コード全文

"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>
  );
};

新規登録して、もっと便利にQiitaを使ってみよう

  1. あなたにマッチした記事をお届けします
  2. 便利な情報をあとで効率的に読み返せます
ログインすると使える機能について

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
新規登録
すでにアカウントを持っている方はログイン
6