はじめに
前回に引き続き、普段Javaを書いているわたしがReactで学んだことを共有します。
今回はuseStateという情報を保持するための機能について確認します。
じゃんけんをしたい
唐突ですが、無性にじゃんけんがしたくなりました。
以下のような仕様をみたすじゃんけんプログラムを作ってみます。
・グーチョキパーの中から自分の手を選択する
・自分の手を選択したタイミングで、相手の手がランダムで出力される
・勝敗を表示する
以下は画面イメージです。(左:初期表示/右:結果表示)
画面の見た目を作るだけならJSXを利用して実装することができそうです。
問題は、どのようにボタンのクリックに応じて画面の結果を変えるか、です。
試しに現状の知識だけでやってみます。
画面のデータを動的に変更できるか
いきなりゴールの画面を作り込む前に、ボタンのクリックに応じて画面の内容を動的に変える方法を考えます。
// じゃんけんの手の値を定義したオブジェクト(後の拡張性のため、このような形)
const HANDS = {
rock: { name: "グー" },
scissors: { name: "チョキ" },
paper: { name: "パー" }
}
// 画面に出力する内容の定義
const ChangeExample = () => {
// じゃんけんの手を変更する関数
const changeHand = (e) => {
myHand = e.target.value;
console.log(HANDS[myHand].name);
}
// 初期値
let myHand = 'rock';
return (
<>
{/* HANDSオブジェクトのキー分、ボタンを生成する繰り返し処理を実施 */}
{Object.keys(HANDS).map(h => {
return (
<button type="button" key={h} value={h} onClick={changeHand}>{HANDS[h].name}</button>
)
})}
{/* HANDSオブジェクトのキー名を変数で指定、nameを取得 */}
<p>自分の手:{HANDS[myHand].name}</p>
</>);
};
export default ChangeExample;
上記はReactの新しい知識は使わずに、JSXとJavaScriptで実装した内容になります。
(※上記のファイルを別のファイルで読み込まないと画面表示ができませんが、ここでは割愛します。)
ボタンを押すと、そのボタンの値を取得して、myHandに再設定する実装になっています。
ボタン押下がわかりやすいよう、コンソールにも値を出力するようにしています。
実行するとわかりますが、これではうまくいきません。
ボタンを押下するとコンソールには値が出力されますが、画面上の値は初期値のままです。
実装の問題点
問題点は2つあります。
1. 画面の再描画をしていない
上記の実装では、画面上に出力している変数の値を書き換えているだけです。
書き換えられた変数の値を出力するためには、再度画面を描画しなくてはなりません。
2.変数の値を画面描画後に持ち越せない
再度画面を描画すると、そのたびに変数が初期化(ここでは「rock」を設定)されます。
画面再描画の後に変数の値を持ち越すことができないので、画面の値を動的に変えられません。
では、どのようにすれば解決できるのでしょうか
状態(state)を管理するuseState
useStateを使ってみる
画面の再描画をする、再描画後も値を保持する
この2つを実現できるのが、Reactのフックという機能の1つであるuseStateです。
まずはuseStateを使って書き換えた結果を見てみます。
import { useState } from "react";
// じゃんけんの手の値を定義したオブジェクト(後の拡張性のため、このような形)
const HANDS = {
rock: { name: "グー" },
scissors: { name: "チョキ" },
paper: { name: "パー" }
}
// 画面に出力する内容の定義
const ChangeExample = () => {
// じゃんけんの手を変更する関数
const changeHand = (e) => {
// set関数を呼ぶことで、画面の再描画がトリガーされる
setMyHand(e.target.value);
// コンソール出力は一旦止めたいのでコメントアウト
// console.log(HANDS[myHand].name);
}
// 初期値(分割代入を使用)
const [myHand, setMyHand] = useState('rock');
return (
<>
{/* HANDSオブジェクトのキー分、ボタンを生成する繰り返し処理を実施 */}
{Object.keys(HANDS).map(h => {
return (
<button type="button" key={h} value={h} onClick={changeHand}>{HANDS[h].name}</button>
)
})}
{/* HANDSオブジェクトのキー名を変数で指定、nameを取得 */}
<p>自分の手:{HANDS[myHand].name}</p>
</>);
};
export default ChangeExample;
書き換えたのは3箇所です。(コンソール出力のコメントアウトは除く)
1. 最上部でuseStateをインポート
2. 初期値の設定にuseStateを使用
3. changeHand関数内で、set関数を使用
以下のように、ボタン押下に応じて画面内容が変化することが確認できます。
useStateの使い方
改めてuseStateの使い方を確認します。
基本形は以下のとおりです。
import { useState } from "react";
const Example = () => {
const [state, setState] = useState(初期値);
return {
// 何らかのJSX
}
}
引数:
初期値、省略可能
省略した場合はundefined
戻り値:
値と値を更新するための関数を持った配列
基本的に分割代入で受け取り、更新用関数の変数名は接頭辞としてsetを使用する
分割代入を使用しない場合は以下
const arr = useState(初期値);
const state = arr[0];
const setState = arr[1];
set関数:
呼び出すことで画面の再描画をトリガーする。
設定した値が次の描画時のstateに設定される。
useStateの注意点
先ほどのソースで、コンソール出力をコメントアウトしていましたが、そのコメントアウトを外して、どのような値が出力されるかを見てみます。
1回目はチョキのボタンを押しましたが、コンソールには「グー」が出力されています。
2回目はパーのボタンを押しましたが、コンソールには「チョキ」が出力されています。
初期値が「グー」であることを考えると、押したボタンではなく、1つ前の値が出力されているようです。
実は、useStateのset関数は即時更新ではありません。
また、set関数を呼んだ時点で即時再描画されるわけでもありません。
値の更新は画面が再描画されるときであり、画面再描画は予約された状態になります。
すべての処理が終わるまで待機し、終わってから画面再描画が実施されます。
そのため、処理が終わるまでは値は更新されず、元の値のままとなります。
const changeHand = (e) => {
// このとき、まだmyHandは更新されていない
setMyHand(e.target.value);
// myHandには更新前の値がそのまま入っている
console.log(HANDS[myHand].name);
}
処理で確認すると、上記のようになります。
更新後の値を参照する事もできますが、冗長になるため今回は割愛します。
set関数は即時更新ではない、ということを覚えておいてください。
じゃんけんをしよう
さて、useStateを使用することで、画面の値を動的に変えられることがわかったので、はじめに確認したじゃんけんシステムを作ってみましょう。
相手の手を管理するためのstate、結果を管理するためのstateの2つを用意すればよさそうです。
JSXとしては以下のようになります。
<>
<div>
自分の手:
{Object.keys(HANDS).map(h => {
return (
<button type="button" key={h} value={h} onClick={battle}>{HANDS[h].name}</button>
)
})}
</div>
<div>
相手の手:{rivalHandName}
</div>
<div>
結果:{battleResult}
</div>
</>
自分の手はボタンとして、相手の手、結果を表示する要素を設定します。
ボタンにはbattle関数を設定しています。この関数で、相手の手を生成し、結果を出力するようにします。
次はstateです。
stateはuseStateを複数呼び出すことで、別々のものとして管理できます。
相手の手、結果を管理するために2回useStateを使用します。
// 相手の手用のstate
const [rivalHandName, setRivalHandName] = useState();
// 結果用のstate
const [battleResult, setBattleResult] = useState();
// ボタン押下時、相手の手を生成し、勝敗を判定する処理を実行
const battle = (e) => {
const rivalHand = generateHand();
setRivalHandName(HANDS[rivalHand].name);
const myHand = e.target.value;
const battleResult = judge(myHand, rivalHand);
setBattleResult(battleResult);
}
それぞれ別の変数にuseStateの戻り値を格納し、battle関数内でそれぞれのset関数を呼び出して値を設定しています。
あとは相手の手をランダムで生成するためのgenerateHand関数、結果判定のためのjudge関数を用意します。また、じゃんけんの手を管理するためのオブジェクトも必要です。
これはReactの知識ではなく、単なるJavaScriptの実装のみです。
それらすべてを記載し、先ほどの実装もあわせると以下のようになります。
import { useState } from "react";
/**
* じゃんけんの手の値を定義したオブジェクト
* 各じゃんけんのキーが更に内部でオブジェクトを持つ
* name:親のキーに紐づく名前
* score:親のキーに対して出された手の勝ち負け(1 勝ち/0 あいこ/-1 負け)
*/
const HANDS = {
rock: { name: "グー", score: { rock: 0, scissors: 1, paper: -1 } },
scissors: { name: "チョキ", score: { rock: -1, scissors: 0, paper: 1 } },
paper: { name: "パー", score: { rock: 1, scissors: -1, paper: 0 } }
}
/**
* ランダムにじゃんけんの手のオブジェクトを生成する
* @return 生成したじゃんけんの手のオブジェクト
*/
const generateHand = () => {
// HANDSオブジェクトを配列に変換。0~2の乱数を生成し、配列からランダムに手のオブジェクトを取得
const randomHand = Object.keys(HANDS)[Math.floor(Math.random() * 3)];
return randomHand;
}
/**
* 自分と相手の手から勝敗を判定して結果を出す
* @param {string} myHand 自分の手
* @param {string} rivalHand 相手の手
* @return 勝敗結果
*/
const judge = (myHand, rivalHand) => {
// HANDSオブジェクトから自分の手に紐づく、相手の手のscoreを取得
const score = HANDS[myHand].score[rivalHand];
switch (score) {
case -1:
return "負け";
case 0:
return "あいこ";
case 1:
return "勝ち";
default:
return "";
}
}
// 画面に出力する内容の定義
const Main = () => {
// 相手の手用のstate
const [rivalHandName, setRivalHandName] = useState();
// 結果用のstate
const [battleResult, setBattleResult] = useState();
// ボタン押下時、相手の手を生成し、勝敗を判定する処理を実行
const battle = (e) => {
const rivalHand = generateHand();
setRivalHandName(HANDS[rivalHand].name);
const myHand = e.target.value;
const battleResult = judge(myHand, rivalHand);
setBattleResult(battleResult);
}
return (
<>
<div>
自分の手:
{Object.keys(HANDS).map(h => {
return (
<button type="button" key={h} value={h} onClick={battle}>{HANDS[h].name}</button>
)
})}
</div>
<div>
相手の手:{rivalHandName}
</div>
<div>
結果:{battleResult}
</div>
</>
)
};
export default Main;
ここまでできれば、じゃんけんシステムの完成です。
これで、いつでも一人でじゃんけんを楽しむことができます。
よりstateの理解を深めたければ、画面にじゃんけんの実施回数や勝敗の集計結果を出力してもいいかもしれません。
まとめ
useStateを使うことで、値を使いまわして画面上の値を動的に変更する方法を学びました。
記事の中ではまだ触れていないこともいくつかあるので、気になる方は公式サイトを確認してみてください。
Reactのフックには他にもいろいろなものがありますが、まずはuseStateを覚えておけば、Reactマスターへの第一歩は踏み出せたといえると思います。
これからもいろいろなReactの機能を学び、フロントエンドの力をつけていきます。