【React 初心者】React で文字当てゲーム!

 

最近 React を勉強していて、何か簡単なものを作りたいと思って、このような文字当てゲームを作りました。


Edit react-typing-game

今回、この記事で実装について解説していきます。

コンポーネントや props はほとんど使っておらず、

  • JSX とは
  • state とは
  • どんな感じに書けるのか

ぐらいの人向けの記事になるかなと思います。


React の導入

React を使うには Node.js が必要になります。

Node.js をインストールした後は、次のコマンドをコマンドラインに入力すると、

カレントディレクトリ(今いるディレクトリ)直下に新たなディレクトリ(my-react-app)が作られます。

npx create-react-app my-react-app

my-react-appの部分は好きな名前で大丈夫です。

cd my-react-appでディレクトリを移動してから、npm startすると、

ローカルホストが立ち上がり、ブラウザで見られるようになります。


React の仕組み

React は JavaScript で HTML を書いてしまおうという発想のライブラリです。

その仕組みについてですが、

さきほどのコマンドで作成されたディレクトリの中身を見てみると、

/public/index.html/src/index.jsというのがあります。

/public/index.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
...
<body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="root"></div>
  <!--
    This HTML file is a template.
    If you open it directly in the browser, you will see an empty page.

    You can add webfonts, meta tags, or analytics to this file.
    The build step will place the bundled scripts into the <body> tag.

    To begin the development, run `npm start` or `yarn start`.
    To create a production bundle, use `npm run build` or `yarn build`.
  -->
</body>
...

この HTML ファイルの<body>タグの中身はからっぽです。

中身を JavaScript で生成して、<div id="root"></div>の中に入れ込むことになります。

/src/index.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

ReactDOM.renderによって、id="root"のタグの中身に<React.StrictMode><App /></React.StrictMode>が入るという仕組みです。


そして、<App />の部分には/src/App.jsの内容が展開されます。

このファイルの基本構造は次のようになります。

/src/App.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import React, { Component } from 'react';
import './App.css';

export default class App extends Component {
  render() {
    return (

    );
  }
}

Componentを継承したAppというクラスをエクスポートしていますが、

実際に展開されるのは、returnの中身に書かれた HTML っぽいもの=JSX です。

関連記事

結局、React とは何なのか?

結局、React とは何なのか?


実装

まずは答えとなりうる文字列のリストを作成し、

その中からランダムで答えを取り出します。

/src/App.js
1
2
3
4
5
import React, { Component } from 'react';
import './App.css';

let answers = [...];  // 答えの一覧
let answer = answers[Math.floor(Math.random()*answers.length)];

次に state を宣言します。

state は「外部とは関わりのない」変数です。

対して props は、関数の引数のように、外部から定義できる変数ですが、今回は使いません。

/src/App.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import React, { Component } from 'react';
import './App.css';

let answers = [...];
let answer = answers[Math.floor(Math.random()*answers.length)];

export default class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      ans: answer,                          // 答えとなる文字列
      display: '_'.repeat(answer.length),   // 表示させる文字列
      inputted: '',   // 入力されたアルファベット
      correct: false  // 正解が出たら true
    };
  }
  ...
}

constructorはクラスの初期化メソッドです。

thisはクラス自体を指しているということでとりあえずいいと思います。


次に出力する HTML となる部分を書きます。

/src/App.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
...
render() {
  const { ans, display, correct } = this.state;

  return (
    <div className="main">
      <p>{correct ? 'Correct!' : ''}</p>
      <p className="answer">{display}</p>
      <div className="alphabets">
        {('abcdefghijklmnopqrstuvwxyz'.split('')).map(value => <button name={value} onClick={this.onInput}>{value}</button>)}
      </div>
      <button className="ans-btn" onClick={this.check}>回答</button>
      <button className="next-btn" onClick={() => this.reset(ans)}>次の問題</button>
    </div>
  );
}

HTML を知っていれば、微妙な差異はあるものの JSX はなんとなく読めると思います。

JSX の中に{}で書いた部分に JavaScript のコードが入っています。

state を使うときには3行目のようにデータを受け取る必要があります。

10行目では、a-z までのアルファベットを記した26個のボタンを配置しています。

ボタンを押したときの動作の定義

onInputメソッドで、アルファベットボタンが押されたときにどのアルファベットが選択されたのかのデータを取得します。

/src/App.js
1
2
3
4
5
onInput = e => {
  this.setState({
    inputted: e.target.name
  });
}

アルファベットのボタンのname属性には各アルファベットが指定してあるので、

これを見ることでどのアルファベットが選択されたかがわかります。


次に、checkメソッドで答えの中に指定したアルファベットが含まれているか確認します。

これは「回答」ボタンを押したときに動作します。

/src/App.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
check = () => {
  const { ans, display, inputted } = this.state;
  let displayTmp = display;

  // 一度押したボタンは無効化する
  document.getElementsByName(inputted)[0].disabled = true;
  
  if (~ans.indexOf(inputted)) {   // 選択したアルファベットが答えに含まれているとき
    for (let k = 0;k <= ans.length-1;k++) {
      let letterIndex = ans.indexOf(inputted, k)
      if (~letterIndex && display[letterIndex] !== ans.charAt(letterIndex)) {
        // 一致した部分を見せる
        displayTmp = displayTmp.slice(0, letterIndex) + ans.charAt(letterIndex) + displayTmp.slice(letterIndex + 1);
      }
    }
    this.setState({
      display: displayTmp
    });
    if (displayTmp === ans) {
      this.setState({
        correct: true
      });
    }
  }
}

13行目のdisplayTmp = ...の部分でthis.setStateをしても、forループがうまくいきませんでした。

そのため、displayTmpを仮に作ってから、後でdisplayを変更しています。


ちなみに、~はビットを反転するもので、~-10になります。

0falseとみなされるのを利用しています。


最後に、「次の問題」ボタンを押したときに動作するresetメソッドです。

別の問題を出題しますが、同じ問題は2度と出ないようにします。

/src/App.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
reset = shown => {
  // 答えのリストからすでに出題した答えを除く
  answers = answers.filter(n => n !== shown);
  // 次に出題する問題の答え
  answer = answers[Math.floor(Math.random()*answers.length)]

  // 出す問題がなくなったとき、「次の問題」ボタンを押せないようにする
  if (answers.length === 1) {
    document.getElementsByClassName('next-btn')[0].disabled = true;
  }

  // すべてのアルファベットボタンを押せるようにする
  const buttons = document.getElementsByClassName('alphabets')[0].getElementsByTagName('button');

  for (let i = 0; i <= 25; i++) {
    buttons[i].disabled = false;
  }

  this.setState({
    ans: answer,
    display: '_'.repeat(answer.length),
    correct: false
  });
}

引数shownが、render内でansを受け取っていますので、

shownにはその時点で出題されている問題の答えの文字列が渡されます。


以上で完成になります。

App.js のソースコード全文
/src/App.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import React, { Component } from 'react';
import './App.css';

let answers = ['tiger', 'kangaroo', 'giraffe'];
let answer = answers[Math.floor(Math.random()*answers.length)];

export default class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      ans: answer,
      display: '_'.repeat(answer.length),
      inputted: '',
      correct: false
    };
  }

  onInput = e => {
    this.setState({
      inputted: e.target.name
    });
  }

  check = () => {
    const { ans, display, inputted } = this.state;
    let displayTmp = display;

    document.getElementsByName(inputted)[0].disabled = true;
    
    if (~ans.indexOf(inputted)) {
      for (let k = 0;k <= ans.length-1;k++) {
        let letterIndex = ans.indexOf(inputted, k)
        if (~letterIndex && display[letterIndex] !== ans.charAt(letterIndex)) {
          displayTmp = displayTmp.slice(0, letterIndex) + ans.charAt(letterIndex) + displayTmp.slice(letterIndex + 1);
        }
      }
      this.setState({
        display: displayTmp
      });
      if (displayTmp === ans) {
        this.setState({
          correct: true
        });
      }
    }
  }

  reset = shown => {
    answers = answers.filter(n => n !== shown);
    answer = answers[Math.floor(Math.random()*answers.length)]

    if (answers.length === 1) {
      document.getElementsByClassName('next-btn')[0].disabled = true;
    }

    const buttons = document.getElementsByClassName('alphabets')[0].getElementsByTagName('button');

    for (let i = 0; i <= 25; i++) {
      buttons[i].disabled = false;
    }

    this.setState({
      ans: answer,
      display: '_'.repeat(answer.length),
      correct: false
    });
  }

  render() {
    const { ans, display, correct } = this.state;

    return (
      <div className="main">
        <p>{correct ? 'Correct!' : ''}</p>
        <p className="answer">{display}</p>
        <div className="alphabets">
          {('abcdefghijklmnopqrstuvwxyz'.split('')).map(value => <button name={value} onClick={this.onInput}>{value}</button>)}
        </div>
        <button className="ans-btn" onClick={this.check}>回答</button>
        <button className="next-btn" onClick={() => this.reset(ans)}>次の問題</button>
      </div>
    );
  }
}

関連記事

【React 初心者】React でタイピング練習アプリ!

【React 初心者】React でタイピング練習アプリ!


React といっても、ほとんどは JavaScript の話になりました。

コンポーネントや props を使えば、もっと React の本質に近づけるような気がします。

とはいえ、やはり JSX で書けること、その中に JavaScript のコードを埋め込めることが便利ですね。

参考になれば幸いです。

ではでは👋