Reactで機能を追加していると、useStateで管理する値がどんどん増えていく。
最初のうちは、
「画面で使う値なら、とりあえずstateにしておけばいいのでは?」
と思っていた。
ただ、麻雀のオーラス条件を計算するアプリを作っていると、
- 4人の持ち点
- 自分の席
- 目標順位
- 本場
- 供託
- 現在順位
- 終局後の点数
- 終局後の順位
- 必要なロン点
- 必要なツモ点
と、扱う値がかなり増えてきた。
ここで全部をstateとして持つのではなく、「保存しておく必要がある値」と「ほかの値から計算できる値」を分けることにした。
今回は、その考え方を整理しておく。
stateとして持つ値
たとえば、4人の持ち点はユーザーが入力する。
const [scores, setScores] =
useState<Scores>({
east: 25000,
south: 25000,
west: 25000,
north: 25000,
});
この値はユーザーの操作によって変わり、その値を次の描画でも覚えておく必要がある。
そのため、stateとして持つ。
同じように、今回のアプリでは次のような値をstateで管理している。
- 持ち点
- 自分の席
- 目標順位
- 本場
- 供託
- 難易度
- 終局方法
- 和了者
- 放銃者
- ロン点
- ツモ時の支払い
- テンパイ者
どれもユーザーが変更できる入力値になっている。
現在順位はstateにしない
一方、現在順位はどうするか。
持ち点が、
東家 32000
南家 24500
西家 23000
北家 20500
なら、順位は点数から計算できる。
const ranking =
calculateRanking(scores);
つまり、
scores(値)
↓
calculateRanking(関数)
↓
ranking(値)
という関係になっている。scoresがあればrankingをいつでも求められるため、順位を別のstateとして保存しておく必要はない。
両方stateにすると同期が必要になる
仮に、点数と順位を両方stateにしたとする。
const [scores, setScores] =
useState<Scores>(initialScores);
const [ranking, setRanking] =
useState<RankingRow[]>(initialRanking);
点数を変更するときは、順位も同時に更新しなければならない。
setScores(newScores);
setRanking(
calculateRanking(newScores),
);
毎回これを忘れずに行えば動く。
ただ、どこか一か所でsetRankingを忘れると、
画面に表示されている点数
↓
32000 / 24500 / 23000 / 20500
画面に表示されている順位
↓
変更前の順位
のように、点数と順位が食い違う可能性がある。
そもそも順位は点数から決まるので、別々に管理しない方が単純になる。
終局後の点数も計算できる
同じ考え方で、終局後の点数もstateにはしていない。
たとえばロンなら、
const finalScores = applyRon(
scores,
winner,
loser,
ronPoints,
honba,
kyotaku,
);
必要になる元の情報はすでにある。
現在の点数
和了者
放銃者
ロン点
本場
供託
これらから終局後点数を計算できる。
ツモならapplyTsumo、流局ならapplyDrawを使う。
if (resultType === "ron") {
return applyRon(...);
}
if (resultType === "tsumo") {
return applyTsumo(...);
}
return applyDraw(...);
終局方法などの入力値だけstateとして持ち、結果は必要なときに計算する形にした。
必要なロン点やツモ点も同じ
オーラス条件として表示している、
- 必要なロン点
- 必要なツモ点
- 最低和了点
も入力値から求められる。
たとえば必要なロン点なら、
const requiredRonList =
findRequiredRonList(
scores,
selfSeat,
goal,
honba,
kyotaku,
);
元になるのは、
現在の点数
自分の席
目標順位
本場
供託
だけ。
これらが変われば、必要点数も変わる。
そのため、必要点数そのものをstateに保存するのではなく、元になる値から再計算する。
stateにするか迷ったときの考え方
自分の中では、次のように考えると分かりやすかった。
その値が、
『ユーザーの操作などによって独立して変わるものか?』
それとも、
『ほかの値さえあれば計算できるものか?』
を見る。
今回のアプリなら、
持ち点
→ ユーザーが入力する
→ state
自分の席
→ ユーザーが選ぶ
→ state
目標順位
→ ユーザーが選ぶ
→ state
現在順位
→ 持ち点から計算できる
→ stateにしない
必要ロン点
→ 持ち点や目標から計算できる
→ stateにしない
終局後順位
→ 終局後点数から計算できる
→ stateにしない
という分け方になる。
useMemoは別の話
今回のコードでは、計算結果の一部にuseMemoを使っている。
const ranking = useMemo(
() => calculateRanking(finalScores),
[finalScores],
);
最初は、
「計算して求める値はuseMemoにする」
という理解をしていた。
ただ、これは少し違う。
単純に次のように書いても、順位をstateとして持たないという考え方自体は同じ。
const ranking =
calculateRanking(finalScores);
useMemoは計算結果を一定の条件で再利用するための仕組みで、基本的には不要な再計算を避けるために使うもの。
つまり、
『stateとして持つか』
と、
『useMemoを使うか』
は別々に考えた方がよさそう。
まず「この値はstateとして保存する必要があるか」を考える。
そのうえで、計算量などを見て必要ならuseMemoを使う、という順番になる。
元になる値を少なくすると分かりやすい
今回この形に整理してみて、一番分かりやすかったのは「正解となる値が一つになる」ことだった。
たとえば順位については、持ち点だけが元データになる。
scores
↓
ranking
↓
currentSelfRank
点数を変更すれば、その結果として順位も変わる。
点数と順位を別々に更新する必要がない。
必要な和了点についても同じ。
scores
selfSeat
goal
honba
kyotaku
↓
必要なロン点・ツモ点
元になる入力値を変更すれば、結果もそこから求め直せる。
機能が増えて値の数が多くなっても、「どれが元データなのか」を意識すると少し整理しやすくなった。
まとめ
Reactで扱う値が増えてきたら、すべてをuseStateへ入れるのではなく、
ほかのstateやpropsから計算できないかを一度考える。
今回の麻雀アプリでは、
入力値
→ stateとして持つ
順位や必要点数など
→ 入力値から計算する
という形にした。
計算できる値までstateとして持つと、元データとの同期を自分で管理する必要が出てくる。
逆に、必要な元データだけをstateとして持てば、変更箇所を減らしやすい。
まだReactの状態管理については勉強中だけれど、今のところは、
「この値は本当に覚えておく必要があるのか?」
と考えるのが、stateを増やしすぎないための一つの目安になりそう。
最後にuseStateとuseMemoの簡単な比較を載せて〆。
useState | useMemo | |
|---|---|---|
| 主な用途 | 値を状態として保持する | 計算結果を再利用する |
| 値が変わるきっかけ | setStateで更新する | 依存する値が変わると再計算される |
| 向いているもの | 入力値、選択状態など | 他の値から求められる計算結果 |
| 今回の例 | 持ち点、自分の席、目標順位 | 順位、必要点数、終局後の点数 |
牛一。

コメント