#8 【React.jsで、BlackJackを作る】:state 管理の方法を見直す
これまで
前回は、ディーラーがカードを引く際のルールを確認しました。
予定を変更してお届けします
今回はディーラーがカードを引く際の処理を実装する予定でしたが、現状のデータ(state)の持ち方では不都合が多く、実装がやりにくくなるため、データの持ち方を見直すことにしました。
BlackJack アプリの sandbox
BlackJack アプリの sandbox は以下です。
この sandbox は、連載が進むのと同時に更新されていきます。
「私も作りたい!」という方は、フォークして一緒に作り始めることができます。
はじめから作りたい方はこちら
初期ハンド(手札)のデータを作る際に問題が発生
初期ハンドの値を決め打ちしていたので、まずはそのデータ作成から取り掛かったところ、問題が発生しました。
state 更新時のライフサイクルの関係で、初期ハンドを作成してもデッキのデータが更新されませんでした。
その時のコードは以下のようなものでした。
export default function Border7() {
const [deck, setDeck] = useState(initialDeck);
const [dealersHand, setDealersHand] = useState([]);
const [playersHand, setPlayersHand] = useState([]);
useEffect(() => {
initDealesHand();
initPlayersHand();
},[])
function initDealersHand(){
const newDeck = deck.slice();
// カードを2枚ひいて、newDeck から要素を削除する処理
setDeck(newDeck); // 要素を削除した newDeck の値で state を更新
}
function initPlayersHand(){
// dealer の処理と同様
}
これを実行した直後に state deck の値を確認すると、Array(50) となっていました。
state deck の中身を確認すると、関数 initDealersHand() の処理による state の更新が反映されていなかったのです。
state 更新時のライフサイクル
こちらの記事にあるように、「値をセットした処理と同じ流れで値を取得しようとしている」状態でした。これ、React あるあるですよね。
何がまずいか「複数の state にまたがる更新がある」
その後、色々試してみたのですが、コードが冗長になったり eslint の警告が頻出したりと、しっくりくるコードにはなりませんでした。
各ハンドとデッキがそれぞれ依存し合い、更新が複数の state にまたがって身動きがとれない状態でした。
フック useReducer() を使うことにしました
上記は公式サイトのドキュメントですが、そこにはユースケースとして以下のように書かれており、まさに今回の場合にうってつけでした。
複数の値にまたがる複雑な state ロジックがある場合
useReducer() のポイント
useReducer() の説明は、上記の公式サイトがありますのでそちらに譲りますが、ポイントを挙げるとすれば以下の点になると思います。
・state やその更新ロジックをまとめることができる
・関数 dispatch() の同一性が保証されている
BlackJack コンポーネントの問題点
現状の BlackJack コンポーネントには state がたくさんあり、それらの更新ロジックも含まれています。
アプリの規模が大きくなってきたことで、Border7 のときにはなかった問題が出てきましたね。
useReducer() を使ってみる
では、早速実装していきましょう。
今回は、デッキやハンドに関わる state や処理を抽出することにします。
また、ディーラーがカードを引く際の処理(case "open" 以下)も少しだけ実装しています。
const initialState = {
deck: initialDeck,
dealersHand: [],
playersHand: [],
isDeclaredStand: false
};
function reducer(state, action) {
switch (action.type) {
case "init":
state = initDealersHand(state);
state = initPlayersHand(state);
return state;
case "hit":
const [newDeck, newHand] = deal(state.deck, state.playersHand, 1);
return { ...state, deck: newDeck, playersHand: newHand };
case "stand":
return { ...state, isDeclaredStand: true };
case "open":
console.log(state.dealersHand);
let total = 0;
state.dealersHand.forEach((card) => {
total += getRankNum(card.rank);
});
console.log(total);
return { ...state };
default:
}
}
使う側(抜粋)
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
dispatch({ type: "init" });
}, []);
function doHit() {
dispatch({ type: "hit" });
}
function doStand() {
dispatch({ type: "stand" });
dispatch({ type: "open" });
}
現在の画面
今回の修正で画面は以下のようになりました。
詳しい変更はこちら
Codesandbox は GitHub連携ができます。
以下は今回のコミットによる差分です。
次回
state や処理を抽出することで、BlackJack コンポーネントが少しスッキリしましたね。
次回こそ、ディーラーがカードを引く処理の実装を行いたいと思います。
この記事があなたのお役に立ちましたら、よろしければサポートをお願いいたします! より良い記事をお届けできるよう、活動費に充てさせていただきます。