最近 React を勉強していて、何か簡単なものを作りたいと思って、このような文字当てゲームを作りました。
今回、この記事で実装について解説していきます。
コンポーネントや 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.html1...2<body>3<noscript>You need to enable JavaScript to run this app.</noscript>4<div id="root"></div>5<!--6This HTML file is a template.7If you open it directly in the browser, you will see an empty page.89You can add webfonts, meta tags, or analytics to this file.10The build step will place the bundled scripts into the <body> tag.1112To begin the development, run `npm start` or `yarn start`.13To create a production bundle, use `npm run build` or `yarn build`.14-->15</body>16...
この HTML ファイルの<body>
タグの中身はからっぽです。
中身を JavaScript で生成して、<div id="root"></div>
の中に入れ込むことになります。
/src/index.js1import React from 'react';2import ReactDOM from 'react-dom';3import './index.css';4import App from './App';5import * as serviceWorker from './serviceWorker';67ReactDOM.render(8<React.StrictMode>9<App />10</React.StrictMode>,11document.getElementById('root')12);1314// If you want your app to work offline and load faster, you can change15// unregister() to register() below. Note this comes with some pitfalls.16// Learn more about service workers: https://bit.ly/CRA-PWA17serviceWorker.unregister();
ReactDOM.render
によって、id="root"
のタグの中身に<React.StrictMode><App /></React.StrictMode>
が入るという仕組みです。
そして、<App />
の部分には/src/App.js
の内容が展開されます。
このファイルの基本構造は次のようになります。
/src/App.js1import React, { Component } from 'react';2import './App.css';34export default class App extends Component {5render() {6return (78);9}10}
Component
を継承したApp
というクラスをエクスポートしていますが、
実際に展開されるのは、return
の中身に書かれた HTML っぽいもの=JSX です。
実装
まずは答えとなりうる文字列のリストを作成し、
その中からランダムで答えを取り出します。
/src/App.js1import React, { Component } from 'react';2import './App.css';34let answers = [...]; // 答えの一覧5let answer = answers[Math.floor(Math.random()*answers.length)];
次に state を宣言します。
state は「外部とは関わりのない」変数です。
対して props は、関数の引数のように、外部から定義できる変数ですが、今回は使いません。
/src/App.js1import React, { Component } from 'react';2import './App.css';34let answers = [...];5let answer = answers[Math.floor(Math.random()*answers.length)];67export default class App extends Component {89constructor(props) {10super(props);11this.state = {12ans: answer, // 答えとなる文字列13display: '_'.repeat(answer.length), // 表示させる文字列14inputted: '', // 入力されたアルファベット15correct: false // 正解が出たら true16};17}18...19}
constructor
はクラスの初期化メソッドです。
this
はクラス自体を指しているということでとりあえずいいと思います。
次に出力する HTML となる部分を書きます。
/src/App.js1...2render() {3const { ans, display, correct } = this.state;45return (6<div className="main">7<p>{correct ? 'Correct!' : ''}</p>8<p className="answer">{display}</p>9<div className="alphabets">10{('abcdefghijklmnopqrstuvwxyz'.split('')).map(value => <button name={value} onClick={this.onInput}>{value}</button>)}11</div>12<button className="ans-btn" onClick={this.check}>回答</button>13<button className="next-btn" onClick={() => this.reset(ans)}>次の問題</button>14</div>15);16}
HTML を知っていれば、微妙な差異はあるものの JSX はなんとなく読めると思います。
JSX の中に{}
で書いた部分に JavaScript のコードが入っています。
state を使うときには3行目のようにデータを受け取る必要があります。
10行目では、a-z までのアルファベットを記した26個のボタンを配置しています。
ボタンを押したときの動作の定義
onInput
メソッドで、アルファベットボタンが押されたときにどのアルファベットが選択されたのかのデータを取得します。
/src/App.js1onInput = e => {2this.setState({3inputted: e.target.name4});5}
アルファベットのボタンのname
属性には各アルファベットが指定してあるので、
これを見ることでどのアルファベットが選択されたかがわかります。
次に、check
メソッドで答えの中に指定したアルファベットが含まれているか確認します。
これは「回答」ボタンを押したときに動作します。
/src/App.js1check = () => {2const { ans, display, inputted } = this.state;3let displayTmp = display;45// 一度押したボタンは無効化する6document.getElementsByName(inputted)[0].disabled = true;78if (~ans.indexOf(inputted)) { // 選択したアルファベットが答えに含まれているとき9for (let k = 0;k <= ans.length-1;k++) {10let letterIndex = ans.indexOf(inputted, k)11if (~letterIndex && display[letterIndex] !== ans.charAt(letterIndex)) {12// 一致した部分を見せる13displayTmp = displayTmp.slice(0, letterIndex) + ans.charAt(letterIndex) + displayTmp.slice(letterIndex + 1);14}15}16this.setState({17display: displayTmp18});19if (displayTmp === ans) {20this.setState({21correct: true22});23}24}25}
13行目のdisplayTmp = ...
の部分でthis.setState
をしても、for
ループがうまくいきませんでした。
そのため、displayTmp
を仮に作ってから、後でdisplay
を変更しています。
ちなみに、~
はビットを反転するもので、~-1
が0
になります。
0
がfalse
とみなされるのを利用しています。
最後に、「次の問題」ボタンを押したときに動作するreset
メソッドです。
別の問題を出題しますが、同じ問題は2度と出ないようにします。
/src/App.js1reset = shown => {2// 答えのリストからすでに出題した答えを除く3answers = answers.filter(n => n !== shown);4// 次に出題する問題の答え5answer = answers[Math.floor(Math.random()*answers.length)]67// 出す問題がなくなったとき、「次の問題」ボタンを押せないようにする8if (answers.length === 1) {9document.getElementsByClassName('next-btn')[0].disabled = true;10}1112// すべてのアルファベットボタンを押せるようにする13const buttons = document.getElementsByClassName('alphabets')[0].getElementsByTagName('button');1415for (let i = 0; i <= 25; i++) {16buttons[i].disabled = false;17}1819this.setState({20ans: answer,21display: '_'.repeat(answer.length),22correct: false23});24}
引数shown
が、render
内でans
を受け取っていますので、
shown
にはその時点で出題されている問題の答えの文字列が渡されます。
以上で完成になります。
App.js のソースコード全文
/src/App.js1import React, { Component } from 'react';2import './App.css';34let answers = ['tiger', 'kangaroo', 'giraffe'];5let answer = answers[Math.floor(Math.random()*answers.length)];67export default class App extends Component {89constructor(props) {10super(props);11this.state = {12ans: answer,13display: '_'.repeat(answer.length),14inputted: '',15correct: false16};17}1819onInput = e => {20this.setState({21inputted: e.target.name22});23}2425check = () => {26const { ans, display, inputted } = this.state;27let displayTmp = display;2829document.getElementsByName(inputted)[0].disabled = true;3031if (~ans.indexOf(inputted)) {32for (let k = 0;k <= ans.length-1;k++) {33let letterIndex = ans.indexOf(inputted, k)34if (~letterIndex && display[letterIndex] !== ans.charAt(letterIndex)) {35displayTmp = displayTmp.slice(0, letterIndex) + ans.charAt(letterIndex) + displayTmp.slice(letterIndex + 1);36}37}38this.setState({39display: displayTmp40});41if (displayTmp === ans) {42this.setState({43correct: true44});45}46}47}4849reset = shown => {50answers = answers.filter(n => n !== shown);51answer = answers[Math.floor(Math.random()*answers.length)]5253if (answers.length === 1) {54document.getElementsByClassName('next-btn')[0].disabled = true;55}5657const buttons = document.getElementsByClassName('alphabets')[0].getElementsByTagName('button');5859for (let i = 0; i <= 25; i++) {60buttons[i].disabled = false;61}6263this.setState({64ans: answer,65display: '_'.repeat(answer.length),66correct: false67});68}6970render() {71const { ans, display, correct } = this.state;7273return (74<div className="main">75<p>{correct ? 'Correct!' : ''}</p>76<p className="answer">{display}</p>77<div className="alphabets">78{('abcdefghijklmnopqrstuvwxyz'.split('')).map(value => <button name={value} onClick={this.onInput}>{value}</button>)}79</div>80<button className="ans-btn" onClick={this.check}>回答</button>81<button className="next-btn" onClick={() => this.reset(ans)}>次の問題</button>82</div>83);84}85}
React といっても、ほとんどは JavaScript の話になりました。
コンポーネントや props を使えば、もっと React の本質に近づけるような気がします。
とはいえ、やはり JSX で書けること、その中に JavaScript のコードを埋め込めることが便利ですね。
参考になれば幸いです。
ではでは