3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

株式会社ネオシステムAdvent Calendar 2024

Day 19

React Hooksを使って、じゃんけんするプログラムを作る

Last updated at Posted at 2024-12-18

はじめに

皆さん、初めまして。
今回初めてQiitaの投稿をいたします。

業務でReactに出会い、半年以上経ちますが、まだまだ知らないことが多々あります。
そんな初心者丸出しの私ですが、実際にReactを使うにあたり、使用機会の多かった以下3つのReact Hooksについて、今回はその概要と実際の使用例をご紹介したいと思います。

  • useState
  • useRef
  • useEffect

useStateとは

useStateは、関数コンポーネントで状態管理をするためのフックです。
第一引数は、状態変数を設定します。
第二引数は、状態を更新するための関数を指定します。
この第二引数で指定した関数に更新したい値を設定することで、useStateフックで管理する値を更新することができます。
値の更新を実行すると、処理終了後に再レンダリングが行われます。

// const [状態変数, 状態を更新するための関数] = useState(初期値);
const [state, setState] = useState(initialValue);

// 値の更新
setState(newValue);

useRefとは

useRefは、関数コンポーネントで参照を管理するためのフックです。
DOM要素アクセスや、以前の状態の保存ができます。
useStateとは異なり、値の更新を実行しても、処理終了後に再レンダリングは行われません。

// const 変数名 = useRef(初期値);
const ref = useRef(initialValue);

// 値の更新
ref.current = newValue;

useEffectとは

useEffectは、ライフサイクル用のフックです。
コンポーネントがマウント、更新、アンマウントされたときに実行する処理を記述することができます。
それぞれのタイミングでコンポーネントがレンダリングされます。
これにより、関数の実行タイミングをレンダリング後まで遅らせることができます。

useEffect(() => {
    // レンダリング後に実行したい処理
},[依存する変数の配列]);

useEffectは第二引数(上記でいうところの[依存する変数の配列])の指定方法に応じて、実行タイミングが異なります。
そのため、用途に応じて使い分けが必要になります。

  • 何も指定しない場合
    マウント、更新、アンマウント毎に実行する。
    ※ユーザが文字入力した内容をリアルタイムに表示するなど、主にレンダリングの度に実行したい処理がある場合に使用します。
useEffect(() => {
    console.log("マウント、更新、アンマウント毎に実行");
});
  • 空の配列を指定した場合
    初回レンダリング時のみ実行する。
    ※データベース等から初期データを取得するなど、主に初回レンダリング時に1度だけ実行したい処理がある場合に使用します。
useEffect(() => {
    console.log("初回レンダリング時のみ実行");
},[]);
  • 1つ以上の値が指定されている場合(値は複数指定可能)
    指定された値がレンダリング前後で変化している場合に実行する。
    ※カウンターのカウントアップを検知してメッセージを表示するなど、監視対象の値が変化するタイミングで実行したい処理がある場合に使用します。
useEffect(() => {
    console.log("依存する変数の配列の値が変わるたびに実行");
},[依存する変数の配列]);

使用例

それでは、上記のReact Hooksを用いて簡単なプログラムを作成していきます。
ここでは環境構築が既に完了している前提で進めます。
※ターミナルで「npm start」を実行し、ブラウザで以下の画面が表示されていれば環境構築は完了です。
React home.png

環境構築から行いたい方は【React入門】ローカル環境でReact開発を始めるには?に手順が詳しくまとめられているので、ぜひこちらをご参照ください。

今回作成するプログラム

今回はCPUとじゃんけんをするプログラムを作成します。
仕様は以下の通りです。

  • 3回勝負
  • CPUの出す手は毎回ランダムとする
  • プレイヤーの出す手とCPUの出す手が同じ場合は引き分けとする
  • 引き分けの場合は、勝利回数にカウントしない
  • 3回戦終了後、勝利回数の多い方が勝者となる
  • 勝利回数が同じ場合は引き分けとする

それでは、この仕様を基にコードを書いていきます。

  • App.js
    まずは、App.jsからです。
    と言っても、今回のプログラムではMainコンポーネントを呼び出すのみとします。
App.js
import React from 'react';
import Main from './main'

const App = () => {
    return (
        <Main />
    );
}

export default App;

  • main.js
    次にApp.jsから呼び出されるmain.jsを書きます。
    ※main.jsは処理が多くなるので、部分ごとに分割します。

    • import部分
      Reactと3つのReact Hooks(useState, useRef, useEffect)をインポートします。
      style.cssを追加する場合、ここで併せてインポートします。

      import React, { useState, useRef, useEffect } from "react";
      import "./style.css"
      

    • 変数の初期化部分

      • 定数
        総対戦回数はお好みで変更してください。

        const GAME_COUNT = 3;                                   // 総対戦回数
        const HANDS = ["グー", "パー", "チョキ"];                // 出す手
        

      • useStateを用いた変数
        主に値の更新を行った後に、再レンダリングを行いたい変数を指定します。

        const [playerHand, setPlayerHand] = useState(null);     // プレイヤーの出す手を管理
        const [cpuHand, setCpuHand] = useState(null);           // CPUの出す手を管理
        const [result, setResult] = useState(null);             // 1対戦の結果を管理
        const [isGame, setIsGame] = useState(true);             // 対戦画面であるかを管理
        const [winner, setWinner] = useState(null);             // 最終的な勝者を管理
        

      • useRefを用いた変数
        主に値の更新を行った後に、再レンダリングを行う必要はない変数を指定します。

        const count = useRef(1);                                // 対戦回数を管理
        const playerCount = useRef(0);                          // プレイヤーの勝利回数を管理
        const cpuCount = useRef(0);                             // CPUの勝利回数を管理
        

    • 関数部分

      • プレイヤーが出す手を選択した際に実行する関数
        プレイヤーの選択した手とCPUの出す手を更新します。
        今回はCPUの出す手をランダムに設定するため、Math.floor(小数点以下を切り捨てる)とMath.random(0以上1未満の範囲で浮動小数点の擬似乱数を返す)を用いました。
        お好みの方法を用いてください。

        const handlePlayerHandClick = (hand) => {
            setPlayerHand(hand);
            const cpuHand = HANDS[Math.floor(Math.random() * HANDS.length)];    // CPUが出す手をランダムに設定
            setCpuHand(cpuHand);
        };
        

      • 対戦結果を判定し、勝者と勝利回数を更新する関数
        1対戦ごとに結果を判定し、勝者と勝利回数を更新します。
        仕様に基づいて、プレイヤーとCPUの出す手が同じ場合は、結果を引き分けとし、勝利回数の更新も行いません。

        const updateMatchResult = (playerHand, cpuHand) => {
            // プレイヤーとCPUの出す手が選択(設定)されていることを確認
            if (playerHand && cpuHand) {
                if (playerHand === cpuHand) {
                    setResult("引き分け");
                } else if (
                    (playerHand === "グー" && cpuHand === "チョキ") ||
                    (playerHand === "パー" && cpuHand === "グー") ||
                    (playerHand === "チョキ" && cpuHand === "パー")
                ) {
                    playerCount.current += 1;
                    setResult("プレイヤーの勝ち");
                } else {
                    cpuCount.current += 1;
                    setResult("プレイヤーの負け");
                }
                setIsGame(false);
            }
        }
        

      • すべての対戦終了時に勝者を更新する関数
        対戦回数と総対戦回数が同じ場合に勝者の更新を行います。
        仕様に基づいて、勝利回数が同じ場合は引き分けとします。

        const updateWinner = () => {
            if (count.current === GAME_COUNT) {
                if (playerCount.current === cpuCount.current) {
                    setWinner("引き分け");
                } else if (playerCount.current > cpuCount.current) {
                    setWinner("プレイヤーの勝利");
                } else {
                    setWinner("CPUの勝利");
                }
            }
        }
        

      • 確認ボタンが押されたときに動作する関数
        結果画面で確認ボタンを押すと、次の対戦画面に進む、または最終結果画面に進むようにします。
        その際、対戦回数をカウントアップし、プレイヤーとCPUの出した手はリセットします。
        また、対戦画面であるかの判定をtrueに更新します。
        ※この関数は結果画面で使用するものであるため、不要の場合は省略可能です。

        const check = () => {
            count.current += 1;
            setPlayerHand(null);
            setCpuHand(null);
            setIsGame(true);
        }
        

      • 戻るボタンが押されたときに動作する関数
        最終結果画面で戻るボタンを押すと、最初の画面に戻るようにします。
        すべての変数を初期値に戻します。
        ※この関数は最終結果画面で使用するものであるため、不要の場合は省略可能です。

        const back = () => {
            count.current = 1;
            playerCount.current = 0;
            cpuCount.current = 0;
            setPlayerHand(null);
            setCpuHand(null);
            setResult(null);
            setWinner(null);
            setIsGame(true);
        }
        

    • 描画部分

      • 対戦画面
        ユーザに出す手の選択を促す文言と、各手のボタンを表示します。

        <div>
            <p className="font-small">出す手を選択してください</p>
            <div className="flex-row">
                <button onClick={() => handlePlayerHandClick("グー")}>グー</button>
                <button onClick={() => handlePlayerHandClick("チョキ")}>チョキ</button>
                <button onClick={() => handlePlayerHandClick("パー")}>パー</button>
            </div>
        </div>
        

      • 結果画面
        1対戦ごとの結果画面です。
        プレイヤーとCPUの出した手と対戦結果を表示します。
        今回は、現在何勝ずつしているのかと確認ボタンも併せて表示します。

        <div>
            <p>プレイヤーの手: {playerHand}</p>
            <p>CPUの手: {cpuHand}</p>
            <p>{result}</p>
            <p>プレイヤー:{playerCount.current}CPU:{cpuCount.current}</p>
            <button onClick={() => check()}>確認</button>
        </div>
        

      • 最終結果画面
        すべての対戦終了後に表示する結果画面です。
        最終的な勝者を表示します。
        今回は戻るボタンも併せて表示します。

        <div className="flex-col">
            <p>{winner}</p>
            <button onClick={() => back()}>戻る</button>
        </div>
        

    以上を踏まえた最終的なmain.jsは以下になります。

main.js
import React, { useState, useRef, useEffect } from "react";
import "./style.css"

const Main = () => {
    const GAME_COUNT = 3;                                   // 総対戦回数
    const HANDS = ["グー", "パー", "チョキ"];                // 出す手
    const [playerHand, setPlayerHand] = useState(null);     // プレイヤーの出す手を管理
    const [cpuHand, setCpuHand] = useState(null);           // CPUの出す手を管理
    const [result, setResult] = useState(null);             // 1対戦の結果を管理
    const [isGame, setIsGame] = useState(true);             // 対戦画面であるかを管理
    const [winner, setWinner] = useState(null);             // 最終的な勝者を管理
    const count = useRef(1);                                // 対戦回数を管理
    const playerCount = useRef(0);                          // プレイヤーの勝利回数を管理
    const cpuCount = useRef(0);                             // CPUの勝利回数を管理

    // プレイヤーが出す手を選択したときの処理
    const handlePlayerHandClick = (hand) => {
        setPlayerHand(hand);
        const cpuHand = HANDS[Math.floor(Math.random() * HANDS.length)];    // CPUが出す手をランダムに設定
        setCpuHand(cpuHand);
    };

    // 確認ボタンが押されたときの処理
    const check = () => {
        count.current += 1;
        setPlayerHand(null);
        setCpuHand(null);
        setIsGame(true);
    }

    // 戻るボタンが押されたときの処理(すべての状態変数を初期化する)
    const back = () => {
        count.current = 1;
        playerCount.current = 0;
        cpuCount.current = 0;
        setPlayerHand(null);
        setCpuHand(null);
        setResult(null);
        setWinner(null);
        setIsGame(true);
    }

    // 対戦結果を判定し、勝者と勝利回数を更新する
    const updateMatchResult = (playerHand, cpuHand) => {
        // プレイヤーとCPUの出す手が選択(設定)されていることを確認
        if (playerHand && cpuHand) {
            if (playerHand === cpuHand) {
                setResult("引き分け");
            } else if (
                (playerHand === "グー" && cpuHand === "チョキ") ||
                (playerHand === "パー" && cpuHand === "グー") ||
                (playerHand === "チョキ" && cpuHand === "パー")
            ) {
                playerCount.current += 1;
                setResult("プレイヤーの勝ち");
            } else {
                cpuCount.current += 1;
                setResult("プレイヤーの負け");
            }
            setIsGame(false);
        }
    }

    // すべての対戦終了時に勝者を更新する
    const updateWinner = () => {
        if (count.current === GAME_COUNT) {
            if (playerCount.current === cpuCount.current) {
                setWinner("引き分け");
            } else if (playerCount.current > cpuCount.current) {
                setWinner("プレイヤーの勝利");
            } else {
                setWinner("CPUの勝利");
            }
        }
    }

    useEffect(() => {
        updateMatchResult(playerHand, cpuHand);
    }, [playerHand, cpuHand]);

    useEffect(() => {
        updateWinner();
    }, [isGame]);

    return (
        <div className="position">
            {/* header */}
            <strong className="font-large">じゃんけん</strong>

            {/* body */}
            {count.current <= GAME_COUNT ? (
                <div className="flex-col">
                    <p>{count.current}回戦</p>

                    {/* 対戦画面 */}
                    {isGame ? (
                        <div>
                            <p className="font-small">出す手を選択してください</p>
                            <div className="flex-row">
                                <button onClick={() => handlePlayerHandClick("グー")}>グー</button>
                                <button onClick={() => handlePlayerHandClick("チョキ")}>チョキ</button>
                                <button onClick={() => handlePlayerHandClick("パー")}>パー</button>
                            </div>
                        </div>
                    ) : (
                        // 1対戦の結果
                        <div>
                            <p>プレイヤーの手: {playerHand}</p>
                            <p>CPUの手: {cpuHand}</p>
                            <p>{result}</p>
                            <p>プレイヤー:{playerCount.current}CPU:{cpuCount.current}</p>
                            <button onClick={() => check()}>確認</button>
                        </div>
                    )}
                </div>
            ) : (
                // 最終結果
                <div className="flex-col">
                    <p>{winner}</p>
                    <button onClick={() => back()}>戻る</button>
                </div>
            )}
        </div>
    );
};

export default Main;
  • style.css
    要素の並び方やフォントサイズを少しいじるために追加していますが、なくても問題ありません。
style.css
/* 縦並び */
.flex-col {
    display: flex;
    flex-direction: column;
    text-align: center;
}

/* 横並び */
.flex-row {
    display: flex;
    flex-direction: row;
    justify-content: center;
}

/* ポジション */
.position {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    inset: 0;
    position: absolute;
}

/* フォントサイズ */
.font-small {
    font-size: small;
}

.font-large {
    font-size: large;
}

実行結果

ターミナルで「npm start」を実行すると、以下の画面が表示されます。
game.png

先程の画面で「グー」「チョキ」「パー」から1つ選択してボタンを押下すると、以下の結果画面が表示されます。
result.png

先程の画面で確認ボタンを押下すると、次の対戦画面に移ります。
next_game.png

指定した回数(今回は3回)の対戦すべてが完了し、結果画面で確認ボタンを押下すると、以下の最終結果画面が表示されます。
戻るボタンを押下すると、再度1回戦から行うことができます。
finish_result.png

終わりに

最後までご覧いただきありがとうございました。
今回はReact Hooksの中でもよく使われるuseState、useRef、useEffectの3つのフックについて、簡単な概要と使用例をご紹介しました。
私自身まだ使い方を模索している部分もあるため、至らない点もあるとは思いますが、これからReactを使う方の参考に少しでもなれたら幸いです。
今回ご紹介した3つのフック以外にも便利なものがあるので、興味を持たれた方はぜひそちらも併せてご活用ください。

参考文献

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?