React でお手軽にタイピングゲームもどきを作る

前々から React の勉強をしたいと思っていたのですが、勉強がてら、とても簡単なタイピングゲームもどきを作ってみたので紹介します。

作ったもの

デモを用意したのでご覧ください。

開発環境

Docker で開発環境を構築しました。参考にしたサイトは記事の最後に載せておきますので参考にしてください。ライブラリのバージョンは下記の通りです。

  • node:19.0.0
  • react:18.2.0

最初に下記コマンドを実行後 src ディレクトリ配下のファイルは全て削除しています。

npx create-react-app my-app

プログラムソース

src/index.js を作成し、そこにコードを作成していきます。小規模なので複数のコンポーネントに分けたりはしていません。まずは、src/index.js の全てを貼ります。

import React from "react";
import ReactDOM from "react-dom/client";

class Typing extends React.Component {
  /**
   * コンストラクタ
   * @param props
   */
  constructor(props) {
    super(props);
    // お題リスト
    this.subjectList = ['apple', 'orange', 'banana'];
    this.state = {
      subjectIndex: 0,                // 現在のお題番号
      inputValue: '',                 // 入力欄テキストボックスの値
      subject: this.subjectList[0],   // お題
      status: '間違ってます。',         // 現在のステータス
    }
  }

  /**
   * ChangeEvent を取り扱う
   * @param {React.ChangeEvent<HTMLInputElement>} e イベントオブジェクト
   */
  handleChange(e) {
    this.setState({inputValue: e.target.value});

    if (e.target.value === this.state.subject) {
      // 正解したら
      this.setState({status: '正解!'});
      setTimeout(() => {this.setNextSubject()}, 1000);
    }
  }

  /**
   * 次のお題を設定する
   */
  setNextSubject() {
    let nextSubjectIndex = this.state.subjectIndex + 1;
    if (nextSubjectIndex >= this.subjectList.length) {
      this.setState({status: '終わり。'});
      return;
    }

    this.setState({
      subjectIndex: nextSubjectIndex,
      inputValue: '',
      subject: this.subjectList[nextSubjectIndex],
      status: '間違ってます。'
    });
  }

  /**
   * 描画する
   * @returns {JSX.Element}
   */
  render() {
    return (
      <div>
        <p>お題:{this.state.subject}</p>
        <p>状況:{this.state.status}</p>

        入力してください。<br></br>
        <input
          type="text"
          onChange={(e) => this.handleChange(e)}
          value={this.state.inputValue}
        />
      </div>
    );
  }
}

// ========================================
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Typing/>);

一つ一つ解説します。

import React from "react";
import ReactDOM from "react-dom/client";

class Typing extends React.Component {
  /**
   * コンストラクタ
   * @param props
   */
  constructor(props) {
    super(props);
    // お題リスト
    this.subjectList = ['apple', 'orange', 'banana'];
    this.state = {
      subjectIndex: 0,                // 現在のお題番号
      inputValue: '',                 // 入力欄テキストボックスの値
      subject: this.subjectList[0],   // お題
      status: '間違ってます。',         // 現在のステータス
    }
  }

まず一番始めに必要なものをインポートします。今回は複数のコンポーネントに分けていないので、クラスは Typing クラス1つだけになります。コンストラクタで設定している this.state ですが、これはコンポーネントの情報(状態)を持つものです。this.state の値を更新すると、その値を利用している箇所を変更(再描画)させることができます。今回 this.state には制御したいもの、つまり、テキストボックスに文字が入力されたら変更(再描画)させたいものを設定しています。例えば、お題通りの文字が入力され正解になったら、次のお題を出題するわけですが、お題番号やお題を書き換えるためにその値をここで管理しています。

  /**
   * 描画する
   * @returns {JSX.Element}
   */
  render() {
    return (
      <div>
        <p>お題:{this.state.subject}</p>
        <p>状況:{this.state.status}</p>

        入力してください。<br></br>
        <input
          type="text"
          onChange={(e) => this.handleChange(e)}
          value={this.state.inputValue}
        />
      </div>
    );
  }

順番が前後しますが、先に render() を解説します。ここで return しているものが画面に表示されています。お題と状況は this.state に設定したものを表示させています。テキストボックスですが、onChange={(e) => this.handleChange(e)} で、文字が入力される度に this.handleChange() が呼ばれるようにしています。value つまりテキストボックスの中身は this.state.inputValue で管理します。これはお題に正解して、次のお題に進む時に、テキストボックスの中身を空にするために利用します。

  /**
   * ChangeEvent を取り扱う
   * @param {React.ChangeEvent<HTMLInputElement>} e イベントオブジェクト
   */
  handleChange(e) {
    this.setState({inputValue: e.target.value});

    if (e.target.value === this.state.subject) {
      // 正解したら
      this.setState({status: '正解!'});
      setTimeout(() => {this.setNextSubject()}, 1000);
    }
  }

テキストボックスに文字が入力される度に呼ばれるメソッドです。まず始めの行ですが、this.setState()this.state.inputValue つまり、テキストボックスの中身を設定しています。e.target.value はテキストボックスに入力された文字列なので、つまり、テキストボックスに入力された値を、そのままテキストボックスに反映させています。テキストボックスの中身を管理しているのは、正解時にテキストボックスを空にしたいから、という理由だけなので、正解時以外は普通に動作するようにします。

そして if 文の中が正解したときの処理になります。正解したら this.state.status を「正解!」に変えて、1秒後に次のお題を出すメソッド this.setNextSubject() を呼びます。なぜ1秒後にしているかというと、すぐに this.setNextSubject() を呼び出すと、その中ですぐに this.state.status が「間違ってます。」に設定されてしまうので、見た目的に「正解!」に変わったように見えないためです。また setTimeout(this.setNextSubject, 1000) と書いても良さそうな気がしますが、そうすると this.setNextSubject() 内の thisundefined 等になってしまうので、アロー関数を渡しています。

  /**
   * 次のお題を設定する
   */
  setNextSubject() {
    let nextSubjectIndex = this.state.subjectIndex + 1;
    if (nextSubjectIndex >= this.subjectList.length) {
      this.setState({status: '終わり。'});
      return;
    }

    this.setState({
      subjectIndex: nextSubjectIndex,
      inputValue: '',
      subject: this.subjectList[nextSubjectIndex],
      status: '間違ってます。'
    });
  }

次のお題を設定するメソッドです。まずお題番号をインクリメントします。お題番号がお題数以上であれば、this.state.status を「終わり。」に変えて処理を終了します。お題番号がお題数未満であれば、お題番号、お題を次のお題に設定し、this.state.status を「間違ってます。」に戻し、this.state.inputValue を空にすることで、テキストボックスの中身を空にします。

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Typing/>);

最後に作ったものを描画して完成です。

今回の作ったタイピングゲームでは、タイプミスしても、特にペナルティがありません。よくあるタイピングゲームだと、タイプミスをしたらエラーになり、正しく入力するまで次の文字に進めない、という動作をすることが多いと思いますので、次回はその機能を実装していきたいと思います。

参考サイト