【React 初心者】React でタイピング練習アプリ!
今回は、このようなタイピング練習アプリを作ってみました。
この記事では、このタイピングアプリの実装について解説しながら、
少々ですが練習のために使用した、
- React Hooks
- コンポーネントと props
- Material-UI
- TypeScript
についても書いていきます。
これらの知識がすでにある方は、最初の4項目は読み飛ばしていただいても構いません。
React Hooks (useState)
前回の文字当てゲームでは、クラスを定義して state を使っていました。
しかし、React Hooks という機能を使うことで、関数の中で state を扱うことができるようになります。
例えば、このように書いていたのを、
/src/App.js1import React, { Component } from 'react';23export default class App extends Component {4constructor(props) {5super(props);6this.state = {7firstState: 1,8secondState: false,9};10}1112setValue = () => {13const { firstState } = this.state;14this.setState({15firstState: firstState + 1,16});17};1819render() {20const { firstState, secondState } = this.state;2122return (23<div>24<p>{firstState}</p>25<p>{secondState}</p>26</div>27);28}29}
次のように簡潔に書くことができるようになります。
/src/App.js1import React, { useState } from 'react';23export default function App() {4const [firstState, setFirstState] = useState(1);5const [secondState, setSecondState] = useState(false);67const setValue = () => setFirstState(firstState + 1);89return (10<div>11<p>{firstState}</p>12<p>{secondState}</p>13</div>14);15}
const [firstState, setFirstState]
のfirstState
は変数(state)名、setFirstState
はその変数を変更させるための関数名です。
useState()
の引数に変数の初期値を渡します。
Hooks を使うことで、関数内部のどこからでも宣言なしでさっと state を参照できて、とても便利です。
コンポーネントと props
コンポーネントとは「部品」のことです。
次のように使います。
1import React from 'react';23function MyComponent() {4return <p>This is my component.</p>;5}67function AnotherComponent() {8return (9<div>10"MyComponent" comes here.11<MyComponent />12</div>13);14}
<MyComponent />
と書いた部分に、
<p>This is my component.</p>
が展開されます。
props は「外部から値を入れられる変数」のことです。
関数の引数みたいなイメージです。
関数を呼び出すときに引数を渡すのと同じように、次のように書きます。
1import React from 'react';23function MyComponent(props) {4return <p>This is my {props.adjective} component.</p>;5}67function AnotherComponent() {8return (9<div>10"MyComponent" comes here.11<MyComponent adjective='great' />12</div>13);14}
これで<MyComponent adjective="great" />
と書いた部分に、
<p>This is my great component.</p>
が展開されます。
なお、3~5行目は props を省略して次のように書くこともできます。
1function MyComponent({ adjective }) {2return <p>This is my {adjective} component.</p>;3}
ちなみに、コンポーネント名の先頭は大文字である必要があります。
Material-UI
Material-UI は、React コンポーネントを使って
を実現できるライブラリです。npm install @material-ui/core
とターミナルに打ってインストールすることで、
すぐに使い始めることができます。
いろいろなコンポーネントが用意されていて、JSX に書き込むだけで、
その位置にきれいなデザインのボタンやカード、ダイアログなどを表示できます。
例えば、ボタンを表示させるには、次のようにします。
1import React from 'react';2import Button from '@material-ui/core/Button';34function ButtonSample() {5const handleClick = () => // doSomething();67return (8<div>9<Button onClick={handleClick} variant="contained" color="primary">10Click me!11</Button>12</div>13);14}
公式ドキュメントには、どうすればどのようなデザインになるのかについて詳しく書かれています。
これで簡単にきれいなデザインを実装することができます。
TypeScript
TypeScript とは、型付けができる JavaScript です。
定数や変数を定義するときに、「これは文字列だ」「これは数値だ」と決めておくことで、
予期せぬエラーを未然に防ぐことができるようになります。
Create React App で TypeScript を使う場合、プロジェクト作成の際に--typescript
オプションをつけるだけで、
TypeScript による開発を始めることができます。
npx create-react-app my-react-app --typescript
実装
さて、ここから実際の実装作業に入っていきます。
ボタンコンポーネントを作る
まずはボタンのためのコンポーネントを作ります。
components
フォルダの下にMyButton.tsx
を作成します。
/src/components/MyButton.tsx1import React from 'react';2import Button from '@material-ui/core/Button';34type Props = {5children: any,6onClick: () => void,7};89export default function MyButton({ children, onClick }: Props) {10return (11<Button onClick={onClick} variant='contained' color='primary'>12{children}13</Button>14);15}
4~7行目では、MyButton
関数の引数の型をまとめて定義しています。
とはいってもany
は「何でも OK」というものなので、あまり意味がなかったりしますが、
だとしても型定義はしないと怒られます。
() => void
は返り値のない関数であることを示します。
ところで Material-UI のButton
は、デフォルトではテキストがすべて大文字になってしまうので、
この設定を解除します。
/src/components/MyButton.tsx1import React from 'react';2import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';3import Button from '@material-ui/core/Button';45const styles = createMuiTheme({6typography: {7button: {8textTransform: 'none',9},10},11});1213type Props = {14children: any,15onClick: () => void,16};1718export default function MyButton({ children, onClick }: Props) {19return (20<ThemeProvider theme={styles}>21<Button onClick={onClick} variant='contained' color='primary'>22{children}23</Button>24</ThemeProvider>25);26}
Button
コンポーネントをThemeProvider
コンポーネントで囲み、
その props のtheme
に、5行目で定義したスタイルを指定しています。
これでボタンにスタイルが適用され、ボタンの文字が大文字化しなくなります。
ここでexport
したコンポーネントを、App.tsx
でインポートして使用します。
/src/App.tsx1import React from 'react';2import MyButton from './components/MyButton';34export default function App() {5return (6<div className='App'>7<MyButton>ON</MyButton>8</div>9);10}
state と構成を決める
今回のアプリで必要となる state は、
- 表示する文字
- ボタンで入力可能・不可能を切り替えるブール変数
- 現在入力している位置
- タイプミスをした位置の配列
となります。
まず、ボタンで入力可能・不可能を切り替えるためのブール変数を用意します。
/src/App.tsx1import React, { useState } from 'react';2import MyButton from './components/MyButton';34export default function App() {5const [typing, setTyping] = useState(false);67const typingToggle = () => setTyping(typing ? false : true);89return (10<div className='App'>11<MyButton onClick={typingToggle}>{typing ? 'OFF' : 'ON'}</MyButton>12</div>13);14}
ボタンを押すとtypingToggle
関数が動作し、typing
を変更します。
ボタンの表示は ON OFF と切り替わります。
次に、タイピング用のテキストを用意します。
これはリセットボタンを押すと変化するようにするので、state に入れています。
/src/App.tsx1import React, { useState } from 'react';2import MyButton from './components/MyButton';34export default function App() {5const [text, setText] = useState('test text');6const [typing, setTyping] = useState(false);78const typingToggle = () => setTyping(typing ? false : true);910return (11<div className='App'>12<div id='textbox'>{text}</div>13<MyButton onClick={typingToggle}>{typing ? 'OFF' : 'ON'}</MyButton>14</div>15);16}
キー入力を処理する
React には合成イベントというものがあり、
キーボードイベントやホイールイベントを処理することができます。
onKeyPress
を使って、とりあえず入力したキーを表示させてみます。
/src/App.tsx1import React, { useState } from 'react';2import MyButton from './components/MyButton';34export default function App() {5const [text, setText] = useState('test text');6const [typing, setTyping] = useState(false);78const typingToggle = () => setTyping(typing ? false : true);910const handleKey = (e: React.KeyboardEvent<HTMLDivElement>) => setText(e.key);1112return (13<div className='App' onKeyPress={(e) => handleKey(e)} tabIndex={0}>14<div id='textbox'>{text}</div>15<MyButton onClick={typingToggle}>{typing ? 'OFF' : 'ON'}</MyButton>16</div>17);18}
onKeyPress
の部分に指定した関数が、なんらかのキーが押されたときに動作します。
tabIndex
を指定しないとキーに反応しないので注意してください。
React.KeyboardEvent<HTMLDivElement>
というのは TypeScript の型定義で、
VS Code であれば Quick Fix 機能を使っていい感じに補完してくれたりします。
これで入力したキーを認識し、表示することができます。
さて、ここからタイピングアプリにしていくので、あらかじめ用意された文字列と比較して、
文字が一致するかどうかで表示を変えることになります。
とりあえず、入力し終わった文字を黒、まだ入力していない文字をグレーで表示させてみましょう。
現在どこの文字を入力しようとしているか(位置)の情報が必要なので、state に追加します。
/src/App.tsx1import React, { useState } from 'react';2import MyButton from './components/MyButton';3import './App.scss';45export default function App() {6const [text, setText] = useState('test text');7const [typing, setTyping] = useState(false);8const [position, setPosition] = useState(0);910const typingToggle = () => setTyping(typing ? false : true);1112const handleKey = (e: React.KeyboardEvent<HTMLDivElement>) => {13// 入力可能のとき14if (typing) {15// 入力したキーと現在入力しようとしている文字が一致するとき16if (e.key === text[position]) {17// 次の位置へ移動18setPosition(position + 1);19}20}21};2223return (24<div className='App' onKeyPress={(e) => handleKey(e)} tabIndex={0}>25<div id='textbox'>26<span className='typed-letters'>{text.slice(0, position)}</span>27<span className='waiting-letters'>{text.slice(position)}</span>28</div>29<MyButton onClick={typingToggle}>{typing ? 'OFF' : 'ON'}</MyButton>30</div>31);32}
今回は SCSS を使っていますが、CSS でも構いません。
SCSS を使うためには、ターミナルでnpm install -D node-sass
と入力しておきます。
正しい文字を打つと文字が黒くなり、正しい文字を打たないと先に進みません。
また、ボタンで文字入力不可の状態にすると、何を打っても反応しなくなります。
打ち間違えた文字を赤くする
打ち間違えた文字を、その瞬間だけ赤くするだけなら、
「入力し終わった文字」「現在入力待ちの文字」「まだ入力されていない文字」の3つに分ければ済みますが、
入力し終わった文字でも打ち間違えていたものは赤くするという場合、
文字ひとつひとつに色付けをしていくことになります。
そのために、ひとつひとつの文字をspan
タグに入れて、そのclassName
を動的に変更することで、
文字が黒かグレーか赤かを決めます。
これを実現するためにmap
メソッドを使い、map
を使うためにsplit
メソッドで文字列を配列に変換します。
ただ、最初の1文字だけははじめから「現在入力待ちの文字」にしています。
/src/App.tsx1import React, { useState } from 'react';2import MyButton from './components/MyButton';3import "./App.scss";45export default function App() {6const [text, setText] = useState("test text");7const [typing, setTyping] = useState(false);8const [position, setPosition] = useState(0);9const [typo, setTypo] = useState(new Array(0));1011const typingToggle = () => setTyping(typing ? false : true);1213const handleKey = (e: React.KeyboardEvent<HTMLDivElement>) => {14// 入力可能のとき15if (typing) {16// 文字の配列を取得17let textSpans = document.querySelector("#textbox")!.children;18// 入力したキーと現在入力しようとしている文字が一致するとき19if (e.key === text[position]) {20// 現在の文字を入力済とする21textSpans[position].classList.add("typed-letters");22textSpans[position].classList.remove("current-letter");23// まだ入力していない文字があるとき24if (position <= text.length - 2) {25// 次の位置へ移動26textSpans[position + 1].className = "current-letter";27setPosition(position + 1);28// 全ての文字を入力し終わったとき29} else {30// 入力不可にする31setTyping(false);32}33// 間違ったキーを入力したとき34} else {35// その位置でのはじめての打ち間違えであるとき36if (typo.indexOf(position) === -1) {37// 打ち間違えた位置の配列にその位置を追加38setTypo([...typo, position]);39// 打ち間違えた文字であることを示す class を追加40textSpans[position].classList.add("typo");41}42}43}44};4546return (47<div className="App" onKeyPress={e => handleKey(e)} tabIndex={0}>48<div id="textbox">49<span className="current-letter">{text[0]}</span>50{text51.split("")52.slice(1)53.map(char => (54<span className="waiting-letters">{char}</span>55))}56</div>57<MyButton onClick={typingToggle}>{typing ? "OFF" : "ON"}</MyButton>58</div>59);60}
17 行目のdocument.querySelector("#textbox")!
の!
は、
document.querySelector("#textbox")
がnull
になる可能性があるために TypeScript に怒られることを避けるためにつけています。
null
でないときにのみ動くようになります。
あとは CSS のほうで色の設定をします。
ついでに、「現在入力待ちの文字」の位置に下線を表示して点滅させます。
だいぶ完成に近づいてきました!
リセットボタンをつくる
リセットすると、すべての文字が入力待ちになり、打ち間違えもリセットされます。
/src/App.tsx1...2const refresh = () => {3// 文字の配列を取得4let textSpans = document.querySelector("#textbox")!.children;5// すべての文字のクラス名を変更6for (const i of textSpans) {7i.className = "waiting-letters";8}9// 最初の文字のクラス名のみ変更10textSpans[0].className = "current-letter";11// 位置を最初に12setPosition(0);13// 打ち間違えた位置の配列をリセット14setTypo(new Array(0));15};1617return (18<div className="App" onKeyPress={e => handleKey(e)} tabIndex={0}>19<div id="textbox">20<span className="current-letter">{text[0]}</span>21{text22.split("")23.slice(1)24.map(char => (25<span className="waiting-letters">{char}</span>26))}27</div>28<div className="buttons">29<MyButton onClick={typingToggle}>{typing ? "OFF" : "ON"}</MyButton>30<MyButton onClick={refresh}>Refresh</MyButton>31</div>32</div>33);
これで主要な機能は完成です!
あとはテキストをランダムに生成したり、スタイルを整えたりすれば完成です。
完成形のコード全文は CodeSandbox で見られます。
この記事が参考になったならうれしいです。
では