見出し画像

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

現在の画面

今回の修正で画面は以下のようになりました。

画像1

詳しい変更はこちら

Codesandbox は GitHub連携ができます。
以下は今回のコミットによる差分です。

次回

state や処理を抽出することで、BlackJack コンポーネントが少しスッキリしましたね。
次回こそ、ディーラーがカードを引く処理の実装を行いたいと思います。

この記事があなたのお役に立ちましたら、よろしければサポートをお願いいたします! より良い記事をお届けできるよう、活動費に充てさせていただきます。