お題
一種類の缶入りジュース(代金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 のコードに落とし込めた解釈である、と言えるでしょう。
まるで、スクリーン上の影絵が、手の動きに合わせて形をかえるのと同じような関係なので、仮に「写像」と呼ぶことにします。
この「つねに」は、数学や物理で言うところの
- $y$ は $x$ の関数であり $y = 2x + 3$ と表せる。
- 数学における恒等式
- 物理における方程式 (例: $m\vec{a}=\vec{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>
);
};