超初心者のためのReactチュートリアル
この記事はReactを学び始めたReact超初心者の私が今からReact学習を始める超初心者の方のために書いたものです。
学習していく中で躓いた所を挙げ、その解決方法を書いていきます。
この記事の対象となる方
1.Reactを学び始めた方
2.TypeScriptを使ってReactを学びたい方
3.Reactでコンポーネント間のデータの受け渡しを学びたい方
4.Reactで簡単なアプリを作りたい方
5.Reactでコンポーネントの表示をボタンなどで切り替えたい方
TypeScriptとReactを使った簡単なアプリを作りながら、私がReactで躓いてしまった所を紹介し、その問題を解決していきます!
※最低限Reactチュートリアルに目を通しておくと理解しやすいと思います。
では、はじめましょう!
事前準備
まずは開発環境を整えます。
以下の項目を実施していきましょう。
1.npmのインストール
Node.jsをインストールするとnpmが使えるようになるので以下からインストールしてください。Reactの環境構築に必須なので必ずインストールして下さい。
Node.jsインストール
2.React&TypeScriptの開発環境を構築
ReactはHTMLやCSSなどとは違い、ブラウザとエディターがあれば開発できる訳ではありません。いろいろなファイルを用意しておかないと使うことができません。TypeScriptも同様です。ブラウザはReactやTypeSctriptを直接認識することができません。なので、ブラウザが認識してくれるように書いたコードを変換する必要があります。このように、あるプログラミング言語から違うプログラミング言語に変換することをトランスパイルというらしいです。
このトランスパイルを行いブラウザでReactで作ったアプリを表示するための環境を作ります。
ここからnpmを多用するので必ずインストールしておいて下さい。
では、環境構築をはじめましょう!
まず、環境構築したいディレクトリ内にpackage.jsonというファイルを作成して下さい。そして、以下の内容をファイル内に書いて下さい。
{
"scripts": {
"start": "webpack-dev-server --hot --inline --watch-content-base --content-base ./dist --open --history-api-fallback",
"build": "webpack",
"watch": "webpack -w",
"gulp": "gulp sass:watch"
},
"devDependencies": {
"@types/react-router-dom": "^5.1.3",
"gulp": "^4.0.2",
"gulp-sass": "^4.0.2",
"gulp-sass-glob": "^1.1.0",
"ts-loader": "^5.4.3",
"typescript": "^3.4.4",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.1",
"webpack-dev-server": "^3.10.3"
},
"dependencies": {
"@types/react": "^16.8.14",
"@types/react-dom": "^16.8.4",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2"
},
"private": true
}
その後ターミナルを開き、このpackage.jsonがあるディレクトリに移動し、
npm install
を実行して下さい。これでディレクトリ内に必要なモジュールがインストールされます。インストール後に
run `npm audit fix` to fix them, or `npm audit` for details
のように表示された場合、使用しているモジュールに脆弱性のあるものが含まれているということなので、
npm audit fix
を実行して脆弱性のあるモジュールをバージョンアップして下さい。
次に同じディレクトリ内にtsconfig.jsとwebpack.config.jsを作成して下さい。
{
"compilerOptions": {
"sourceMap": true,
// TSはECMAScript 5に変換
"target": "es5",
// TSのモジュールはES Modulesとして出力
"module": "es2015",
// JSXの書式を有効に設定
"jsx": "react",
"moduleResolution": "node",
"lib": [
"es2020",
"dom"
]
}
}
module.exports = {
// モード値を production に設定すると最適化された状態で、
// development に設定するとソースマップ有効でJSファイルが出力される
mode: "production",
// メインとなるJavaScriptファイル(エントリーポイント)
entry: "./src/main.tsx",
// ファイルの出力設定
output: {
// 出力ファイルのディレクトリ名
publicPath: "/js/",
path: `${__dirname}/dist/js`,
// 出力ファイル名
filename: "main.js",
},
module: {
rules: [
{
// 拡張子 .ts もしくは .tsx の場合
test: /\.tsx?$/,
// TypeScript をコンパイルする
use: "ts-loader"
}
]
},
// import 文で .ts や .tsx ファイルを解決するため
resolve: {
extensions: [".ts", ".tsx", ".js", ".json"]
},
performance: {
maxEntrypointSize: 500000,
maxAssetSize: 500000,
},
};
最後にdistディレクトリとsrcディレクトリを作成して下さい。
また、dist内にはjsディレクトリを作成して下さい。
distはhtml/css/jsファイルを格納する場所です。
srcはTypeScriptのファイルを格納する場所です。
現在のディレクトリの構成が以下のようになっていれば環境構築は完了です。
root
├── dist
│ └── js
├── node_modules
├── package-lock.json
├── package.json
├── src
├── tsconfig.json
└── webpack.config.js
この構成の詳しい内容は割愛しますが、これでReactとTypeScriptを使った最低限の開発環境が完成しました。
この構築は以下のサイトの構築を参考にし、少しだけ手を加えています。
引用元:最新版TypeScript+webpack 4の環境構築まとめ(React, Vue.js, Three.jsのサンプル付き)
Reactが動くことを確認しよう!
環境構築が完了したので、簡単なコードを書いてReactがブラウザで動くことを確認しましょう。
まず、以下のindex.htmlをdistディレクトリ内に作成します。
main.jsはTypeScriptをトランスパイルしたファイルです。
今はまだTypeScriptファイルがないため、main.jsはありません。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React Sample App</title>
<script defer src="js/main.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
はい!、ここで私の躓きポイントです!!!
<script defer src="js/main.js"></script>
ここでdeferが指定されていると思います。本環境でReactをブラウザで表示させようとするとdefer
なしではmain.jsが読み込まれずReactのコンポーネントがレンダリングされませんでした。ちゃんと確認できている訳ではありませんがdeferなしの場合、レンタリングするための領域<divid="app></div>"
が読み込まれるより前にmain.jsが実行されたため、レンダリングされなかったのだと思います。deferはHTMLが全て読み込まれた後にjsを実行するため、この問題を解決できたのだと思います。
この後、TypeScriptファイルを作成してReactが動くことを確認するので、その時にdefer
を外してみて下さい。恐らく、ブラウザに何も表示されないと思います。
ここからはTypeScriptでReactを使っていきます!
main.tsxとtest.tsxを作成して動作確認します。
main.tsxはscrディレクトリに保存して下さい。
test.tsxはscrディレクトリ内にscreensというディレクトリを作成してそこに保存して下さい。
ここまででディレクトリは以下の構成になっているはずです。
root
├── dist
│ ├── index.html
│ └── js
├── node_modules
├── package-lock.json
├── package.json
├── src
│ ├── main.tsx
│ └── screens
│ └── Test.tsx
├── tsconfig.json
└── webpack.config.js
保存した後に、ターミナルで以下のコードを入力し実行して下さい。
npm start
実行後、ブラウザにテストと表示されると成功です!
//reactとreact-domの機能を使うためにインポートします
import * as React from 'react';
import * as ReactDOM from 'react-dom';
//Testコンポーネントをインポートしてmain.tsx内で使用できるようにしています。
import Test from './screens/Test';
//React.Componentは上でReactをインポートしたことによって使用できます。
//これはReactのコンポーネントをAppという名前で作成するためのものです。
//class 任意の名前 extends React.Componentで作成します。
class App extends React.Component {
//Reactで画面に表示させたいときは以下のようにrender,returnを使います。
render() {
return (
<div className="container">
<Test />//JSX構文
</div>
);
}
}
//ReactDomも上でReact.Domをインポートしたことによって使用できます。
//このコードはindex.html内のid="app"をもつ要素内にAppコンポーネントをレンダリングするという意味です。
ReactDOM.render(<App/>, document.querySelector('#app'));
import * as React from 'react';
class Test extends React.Component {
render () {
return (
<h1>テスト</h1>
);
}
}
//このコードを書くことにより他のファイルからTestコンポーネントを使用できるようになります。
export default Test;
tsxについて
拡張子.tsxはTypeScriptとJSXという構文を同時に使う時に用いる拡張子です。
JSXを使うときは拡張子を.tsxにして下さい。
JSXについてはReactチュートリアルで確認して下さい。
ウサギとカメゲームを作ろう!
テストで動作確認できたので実際にアプリを作っていきます。
今回はウサギとカメが闘うゲームを作ります。
構成は
ゲームスタート画面
バトルフィールド画面
結果画面
の3つの画面を作っていきます。
完成したアプリはこのようになっています。
ウサギとカメ
このアプリを作りながらReactを学んでいきましょう。
reactではブラウザをリロードせずに簡単に画面の切り替えを行えます。
その機能を使って3つの画面を作っていきます。
今回はHTML、CSSの解説はしないのでGitHubからCSSファイルをクローンもしくはコピーしておいて下さい。
CSSファイル
また、画像ファイルもダウンロードしておいて下さい。
画像ファイル
状態を持たないコンポーネントもあり、そのときは関数コンポーネントを使用した方がいいと思うのですが、
今回はClassコンポーネントで統一します。
スタート画面
最初にスタート画面を作っていきます。
機能としてはタイトルを表示してGameStartボタンを押すとバトルフィールド画面に遷移するという簡単なものです。
早速実装していきましょう!
まず、動作確認で作成したmain.tsxを以下のように書き換えます。
先に実装するコンポーネントをmain.tsxに追加して、今使わないものはコメントアウトしておきます。
実装した際にコメントアウトを解除していって下さい。
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import GameStart from './screens/GameStart';
//import Field from './screens/Field';
//import TurtleWin from "./screens/Turtle_win";
//import RabbitWin from "./screens/Rabbit_win";
class App extends React.Component {
render() {
return (
<BrowserRouter>
<Switch>
<Route exact path="/" component={GameStart} />
{/*<Route exact path="/field/" component={Field} />*/}
{/*<Route exact path="/Turtle_win/" component={TurtleWin} />*/}
{/*<Route exact path="/Rabbit_win/" component={RabbitWin} />*/}
</Switch>
</BrowserRouter>
);
}
}
ReactDOM.render(<App/>, document.querySelector('#app'));
<BrowserRouter>,<Switch>,<Route>
が新しく出てきたと思います。
これはReactで画面の切り替えを実装する際に使います。
<BrowserRouter>
<Switch>
<Route exact path="/" component={GameStart} />
</Switch>
</BrowserRouter>
この部分はURLが"/"となったときcomponent GameStartを表示するよという意味です。
この環境ではlocalhost:8080
で動作確認しているので、
localhost:8080/
のときGameStartを表示します。
react-router-dom
で画面を切り替える際に注意点が一つあります。
react-router-dom
の画面切り替えではデフォルトの状態だと直接URLを指定するとページが取得できません。
例えばlocalhost:8080/field/
のように直接fieldに遷移しようとした時などです。
しかし、この環境ではローカル環境のサーバー設定で
この問題を回避しています。
{
"scripts": {
"start": "webpack-dev-server --hot --inline --watch-content-base --content-base ./dist --open --history-api-fallback",
package.json内の--history-api-fallback
の部分です。
サーバー側でフォールバックの設定をしてあげることで
localhost:8080/field/
のように直接URLを指定してもそのページ移動できます。
これはローカルでの設定なので、もし本番の環境に移行した場合はそのサーバーでフォールバックを設定して下さい。
今回はすでに設定してあるのでこのような仕様があるといことを覚えておいて下さい。
次にGameStartコンポーネントを作成していきます。
GameStart.tsxはscreensディレクトリに保存して下さい。
import * as React from "react";
import { Link } from 'react-router-dom';
class GameStart extends React.Component {
render() {
return (
//classの代わりにclassNameでクラス名を指定
<div className="l-start">
<h1 className="p-start__title -view">ウサギとカメ</h1>
<button className="p-start__button -view"><Link to="/field" className="p-start__link">Game Start</Link></button>
</div>
);
}
}
export default GameStart;
import { Link } from 'react-router-dom';
でimportしているLinkは画面を遷移させたい時に使います。
ブラウザ上ではaタグとして表示されます。
使い方は、要素をLinkでかこい、to="遷移先"
で遷移するURLを指定します。
今回はボタンを押すことでバトルフィールドに遷移したいので、to="/field"
を指定しています。
次にclassNameについて説明します。
要素にクラス名をつける場合、class="クラス名"
で指定すると思うのですが、Reactでclassは別のところで使用しているため、
クラス名のためにclassが使用できません。ですから、Reactでクラス名を指定する際はclassNameを使います。
ファイルを保存したら以下のコマンドを実行して下さい。
スタート画面が表示されると思います。
npm start
バトルフィールド画面
バトルフィールドはGameStartコンポーネントからから遷移します。
移動後ウサギとカメのバトルが始まりましすが、この画面は3つのコンポーネントでできています。
1.Fiiedコンポーネント
2.HPコンポーネント
3.Commentコンポーネント
この3つの関係は
Fieldコンポーネントが親コンポーネントでHP、Commentコンポーネントがその子コンポーネントになっています。
Fieldコンポーネントで計算した値などを子コンポーネントに渡して画面の表示を切り替えていきます。
では、まずFieldコンポーネントとHPコンポーネントを作成します。
以下のコードからTypeScriptならではの書き方が出てきますが一旦スルーして下さい。
すぐに説明します。
import * as React from "react";
import HP from "./HP";
interface FieldState {
TurtleHP: number;
RabbitHP: number;
}
class Field extends React.Component<{},FieldState> {
constructor(props:{}) {
super(props);
this.state = {
TurtleHP: 5,
RabbitHP: 5,
}
}
render () {
interface CharacterHP {
width:string;
}
let TurtleHP:CharacterHP = {
width:`${this.state.TurtleHP}rem`,
}
let RabbitHP:CharacterHP = {
width:`${this.state.RabbitHP}rem`,
}
return (
<div className="l-field">
<div className="p-field">
<div className="p-field__wrapper">
<div className="p-field__character-box -turtle">
<img src={`${window.location.origin}/images/turtle.png`} alt="キャラクターの画像" className="p-field__character -turtle"/>
<HP CharacterHP = {TurtleHP} />
<button className="p-field__button -view">たたかう</button>
</div>
<div className="p-field__character-box -rabbit">
<HP CharacterHP = {RabbitHP} />
<img src={`${window.location.origin}/images/rabbit.png`} alt="キャラクターの画像" className="p-field__character -rabbit"/>
</div>
</div>
</div>
</div>
);
}
}
export default Field;
import * as React from "react"
interface HPProps {
CharacterHP: {},
}
class HP extends React.Component<HPProps,{}> {
render (){
return(
<div className="l-hp">
<p>HP:</p>
<div className="p-hp__box">
<div className="p-hp__bar -view" style = {this.props.CharacterHP}></div>
</div>
</div>
);
}
}
export default HP;
ここからは細かく解説していきます。
FieldコンポーネントはカメとウサギのHPを保存しています。
このように何か値を保存しておきたい場合はstateを使います。
stateを使う場合はクラスコンポーネントの中にコンストラクタを作成します。
constructor(props:{}) {
super(props);
this.state = {
TurtleHP: 5,
RabbitHP: 5,
}
}
コンストラクタを使用するときはsuper(props)
から始めるのがお約束となっています。
その後にthis.state=...
とつなげていくことでstateを使用できます。
ここではカメとウサギのHPを5としてstateに保存しています。
このHPの値を更新していきキャラクターがダメージ受けるたびにHPバーが減っていく機能を実装します。
書き方は
constructor(props:{}) {
super(props);
this.state = {
任意の名前: 任意の値(文字列や数値など),
}
}
という風に書きます。
ここでTypeScriptの型について軽く説明します。
TypeScriptは静的型付け言語の一種で変数などに型を宣言する必要があります。
宣言の仕方は
const name: string = "yamabaku";
const age: number = 24;
のように宣言します。
コンストラクタの部分ではpropsの後にprops:{}
という風に型を宣言しています。
propsは親コンポーネントからデータを受け取る際に使用するのですが、Fieldコンポーネントは親からデータを受け取る必要がないので空のオブジェクトを型として宣言しています。
次にstateの型宣言について説明します。
stateの型を宣言する場合はinterfaceを使います。
interface FieldState {
TurtleHP: number;
RabbitHP: number;
}
class Field extends React.Component<{},FieldState> {
constructor(props:{}) {
super(props);
this.state = {
TurtleHP: 5,
RabbitHP: 5,
}
}
クラスを作成する前にinterfaceで型を宣言し、
class Field extends React.Component<{},FieldState>
のように書くことでクラス内で宣言した型を使用できます。
{}
の部分はpropsのinterfaceで使用しますが今回はpropsがないため
空のオブジェクトを入れています。
型については以下の記事を参考にしました。
参考:TypeScriptの型入門
次にrender部分を解説していきます。
render () {
interface CharacterHP {
width:string;
}
let TurtleHP:CharacterHP = {
width:`${this.state.TurtleHP}rem`,
}
let RabbitHP:CharacterHP = {
width:`${this.state.RabbitHP}rem`,
}
ここのコードはHPコンポーネントに送るためのオブジェクトを作成しています。
Fieldコンポーネントのstateを使用するときはthis.state.TurtleaHP
というように使います。
カメとウサギがダメージを受けたときここのHPが変化し、その変化したHPをHPコンポーネントに送ることで
HPバーの表示を変化させます。
次にreturn部分です。
return (
<div className="l-field">
<div className="p-field">
<div className="p-field__wrapper">
<div className="p-field__character-box -turtle">
<img src={`${window.location.origin}/images/turtle.png`} alt="キャラクターの画像" className="p-field__character -turtle"/>
<HP CharacterHP = {TurtleHP} />
<button className="p-field__button -view">たたかう</button>
</div>
<div className="p-field__character-box -rabbit">
<HP CharacterHP = {RabbitHP} />
<img src={`${window.location.origin}/images/rabbit.png`} alt="キャラクターの画像" className="p-field__character -rabbit"/>
</div>
</div>
</div>
</div>
);
まずimagタグのpathのですが、Reactを本番環境(実際のサーバー上で動作させる)ときに普通にpathを指定するだけでは読み込んでくれません。
しかし、以下のように指定してあげると読み込んでくれます。
src={
${window.location.origin}/images/turtle.png}
ここはローカル環境でのみ動作させる場合は関係ないのですが、もし本番環境でimgタグがうまく動作していないと思ったらここを思い出して下さい。
他にも画像をimportを使ってコンポーネントに読み込んでからimgタグのsrcを指定する方法もありますので調べてみて下さい。
次にReactのとても便利な機能である、コンポーネント間のデータの受け渡しを解説します。
このコードでデータの受け渡しを行っているのは以下の部分です。
<HP CharacterHP = {TurtleHP} />
ここはHPコンポーネントをレンダリングする時にHPコンポーネントにTurtleHPというデータを渡してレンダリングしてねという意味です。
このCharacterHP
がHPコンポーネントのpropsになります。propsは先ほども説明したように親から子に渡されたデータです。ここではFieldが親でHPが子ですね。
ここでも私が躓いたポイントがあります。
<HP CharacterHP = {TurtleHP} />
という風に記述し、子コンポーネントにデータを渡す際は、その子コンポーネントで以下のようにinterfaceを使ってpropsの型を宣言しておく必要があります。
interface HPProps {
CharacterHP: {},
}
こうしておかないとField.tsxをコンパイルする際にデータを送り先がないよということでエラーが出てしまいます。
例えば上のinterfaceを削除してコンパイルすると
TS2769: No overload matches this call.
Overload 1 of 2, '(props: Readonly<{}>): HP', gave the following error.
Type '{ CharacterHP: CharacterHP; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
Property 'CharacterHP' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
Overload 2 of 2, '(props: {}, context?: any): HP', gave the following error.
Type '{ CharacterHP: CharacterHP; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
Property 'CharacterHP' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
ERROR in /Users/username/Desktop/Turtle-VS-Rabbit/src/screens/Field.tsx
./src/screens/Field.tsx
[tsl] ERROR in /Users/username/Desktop/Turtle-VS-Rabbit/src/screens/Field.tsx(38,33)
TS2769: No overload matches this call.
Overload 1 of 2, '(props: Readonly<{}>): HP', gave the following error.
Type '{ CharacterHP: CharacterHP; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
Property 'CharacterHP' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
Overload 2 of 2, '(props: {}, context?: any): HP', gave the following error.
Type '{ CharacterHP: CharacterHP; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
Property 'CharacterHP' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
のようなエラーが出ます。
このようなエラーが出た場合はデータを渡す子コンポーネントでpropsの型を宣言しているか確認してみて下さい。
ここで一旦HPコンポーネントの解説に移りましょう。
HPコンポーネントは先に説明したようにFieldコンポーネントからCharacterHP
を受け取ります。
その値は以下のstyle
の値として使用します。
propsを使う際もstateと同様にthis.props.CharacterHP
というように指定することでそのデータを扱うことができます。
class HP extends React.Component<HPProps,{}> {
render (){
return(
<div className="l-hp">
<p>HP:</p>
<div className="p-hp__box">
<div className="p-hp__bar -view" style = {this.props.CharacterHP}></div>
</div>
</div>
);
}
}
HPバーのstyle属性をpropsで変動させることによりHPバーが減っていく動きを作っています。
Reactではstateやpropsが変化したことを感知して自動でレンダリングし直してくれます。
ここではその機能を使っています。
ReactのコンポーネントのCSSをどのように当てるかは以下の記事が大変参考になりました。
参考:Reactのコンポーネントのスタイリングをどうやるか
それではFieldコンポーネントに戻ってHPの値を変化させる機能を実装しましょう。
以下のコードではキャラクターの残りHPと攻撃したキャラクターの名前を更新し、残りHPが0になったらResult画面へ遷移する関数、攻撃したキャラクターとそのダメージを表示するCommentコンポーネントを追加しています。
以下で細かく解説していきます。
import * as React from "react";
import HP from "./HP";
import Comment from "./Comment";
interface FieldState {
TurtleHP: number;
RabbitHP: number;
//追加
Damage: number;
name: string;
ShowFlag: boolean;
ClickFlag: boolean;
}
class Field extends React.Component<{},FieldState> {
constructor(props:{}) {
super(props);
this.state = {
TurtleHP: 5,
RabbitHP: 5,
//追加
Damage: null,
name: '',
ShowFlag: false,
ClickFlag: true,
}
this.RabbitAttack = this.RabbitAttack.bind(this)
}
//カメが与えるダメージを決めウサギの残りHPを計算する関数
TurtleAttack ():void {
if(this.state.ClickFlag) {
this.setState({ClickFlag: false});
const Damage:number[] = [10,15,20,25];
let TurtleAttack = Damage[Math.floor(Math.random()* Damage.length)];
this.setState({Damage: TurtleAttack});
this.setState({name: 'カメイ・ウェザー'})
let RestHP = this.state.RabbitHP - TurtleAttack / 20;
if (RestHP > 0){
this.setState({RabbitHP: RestHP});
} else {
this.setState({RabbitHP: 0});
location.href ="/Turtle_win"
}
setTimeout(this.RabbitAttack ,800);
}else {
this.setState({ClickFlag:false});
}
}
//ウサギが与えるダメージを決めカメの残りHPを計算する関数
RabbitAttack ():void{
const Damage:number[] = [15,15,15,15,15,20,20,20,20,20,100000000];
let RabbitAttack = Damage[Math.floor(Math.random()* Damage.length)];
this.setState({Damage: RabbitAttack});
this.setState({name: 'バニー・パッキャオ'})
let RestHP = this.state.TurtleHP - RabbitAttack / 20;
if (RestHP > 0){
this.setState({TurtleHP: RestHP});
} else {
this.setState({TurtleHP: 0});
setTimeout(()=>{location.href ="/Rabbit_win"},500);
}
this.setState({ClickFlag: true});
}
render () {
interface CharacterHP {
width:string;
}
let TurtleHP:CharacterHP = {
width:`${this.state.TurtleHP}rem`,
}
let RabbitHP:CharacterHP = {
width:`${this.state.RabbitHP}rem`,
}
//追加
let ShowDamage = this.state.ShowFlag ? <Comment Damage = {this.state.Damage} name = {this.state.name} /> : '';
return (
<div className="l-field">
<div className="p-field">
<div className="p-field__wrapper">
<div className="p-field__character-box -turtle">
<img src={`${window.location.origin}/images/turtle.png`} alt="キャラクターの画像" className="p-field__character -turtle"/>
<HP CharacterHP = {TurtleHP} />
{/* onClick追加 */}
<button className="p-field__button -view" onClick = { () => {this.TurtleAttack(); this.setState({ShowFlag:true});}}>たたかう</button>
</div>
<div className="p-field__character-box -rabbit">
<HP CharacterHP = {RabbitHP} />
<img src={`${window.location.origin}/images/rabbit.png`} alt="キャラクターの画像" className="p-field__character -rabbit"/>
</div>
</div>
</div>
{/* 追加 */}
{ShowDamage}
</div>
);
}
}
export default Field;
まず、interfaceで宣言するstateが増えているのがわかると思います。
以下の目的で追加しています。
interface FieldState {
TurtleHP: number;
RabbitHP: number;
Damage: number;//相手に与えるダメージを保存
name: string;//攻撃したキャラクターの名前を保存
ShowFlag: boolean;//Commentコンポーネントの表示、非表示を制御
ClickFlag: boolean;//ボタンクリックの連打を防ぐためのものです
}
次に追加した関数、TurtleAttack ()
,RabbitAttack ()
について解説します。
この関数はrender内のbuttonがクリックされた時に呼び出されます。
<button className="p-field__button -view" onClick = { () => {this.TurtleAttack(); this.setState({ShowFlag:true});}}>たたかう</button>
このボタンはクッリクした時にTurtleAttack()
を呼び出し、this.setState({ShowFlag:true}
により
ShowFlag
を更新します。ShowFlag
はCommentコンポーネントで解説します。
まずボタンを押したときの流れを確認します
ボタンクリック→TurtleAttack()→ダメージ、残りHP計算→RabbitAttack()→ダメージ、残りHP計算...→結果画面に遷移という処理になっています。
結果画面は最後に実装します。
まず、関数にコメントを追加し解説していきます。
その前に1つ説明しておきます。
stateの値を更新するためには必ず
this.setState({stateの名前: 更新する値})
というようにstateを更新します。
これを使って以下の関数では値を更新していきます。
では関数をみていきましょう。
TurtleAttack ():void {
//ClickFlagを設けこのフラグがtrueの間のみクリックを受け付けます。
//これによりボタン連打による誤動作を防ぎます。
if(this.state.ClickFlag) {
//ここでClickFlagをfalseとすることでこの関数の実行中はボタンのクリックを受け付けません
this.setState({ClickFlag: false});
//カメが与えるダメージの配列を作成します。
const Damage:number[] = [10,15,20,25];
//上の配列からランダムに値を取り出し、カメがウサギに与えるダメージを決めます。
let TurtleAttack = Damage[Math.floor(Math.random()* Damage.length)];
//そのダメージをと攻撃する側のキャラクターの名前を保存します。
//この2つの値はCommentコンポーネントに送り、攻撃時のコメントとして表示します。
this.setState({Damage: TurtleAttack});
this.setState({name: 'カメイ・ウェザー'})
//ランダムで取り出したダメージを現在のHPから引いて残りのHPを計算しています。
//20で割っているのはHPを100と設定しているからです。HPバーの初期値は5rem(50px)です
//widthを10remとすると大きすぎるので、20で割って5remにしています。
//ダメージの値は一桁より二桁の方が見栄えがいいと思ったのでこのように計算しています。
let RestHP = this.state.RabbitHP - TurtleAttack / 20;
if (RestHP > 0){
//残りHPが0以上の場合は上で計算した値をそのまま現在のHPとして保存します。
this.setState({RabbitHP: RestHP});
} else {
//残りHPが0以下になった場合はHPを0とし結果画面に遷移させます。
this.setState({RabbitHP: 0});
location.href ="/Turtle_win"
}
//カメの与えたダメージを0.8秒間表示してウサギの攻撃に移ります。
//ウサギの攻撃もカメと同様に計算し、最後にClickFlagをtrueにしてボタン操作を再受付けします
//ウサギの攻撃はClickFlagをtrueにする以外ほぼ同じなので説明は省きます。
setTimeout(this.RabbitAttack ,800);
}else {
this.setState({ClickFlag:false});
}
}
TurtleAttack ()
は上記のようになっています。
ここでまた私が躓いたポイントです。
TurtleAttack ()
のsetTimeoutのなかでthis.RabbitAttack()
を呼び出しています。
ここはそのままだとうまく動作しません。thisの意味が変わってしまっているからです。
ですから、constractor内で以下のようにthisを固定してあげる必要があります。
constructor(props:{}) {
super(props);
this.state = {
TurtleHP: 5,
RabbitHP: 5,
Damage: null,
name: '',
ShowFlag: false,
ClickFlag: true,
}
this.RabbitAttack = this.RabbitAttack.bind(this)
}
これでネストが深いところでもthisで関数が使えます。
ここは以下のサイトがとても参考になりました。
React のクラスコンポーネントの bind は何を解決しているのか
これで関数の実装は完了です。
次にCommentコンポーネントについてみていきます。
説明済みの部分は省略しています。
render () {
//省略
let ShowDamage = this.state.ShowFlag ? <Comment Damage = {this.state.Damage} name = {this.state.name} /> : '';
return (
<div className="l-field">
<div className="p-field">
<div className="p-field__wrapper">
<div className="p-field__character-box -turtle">
<img src={`${window.location.origin}/images/turtle.png`} alt="キャラクターの画像" className="p-field__character -turtle"/>
<HP CharacterHP = {TurtleHP} />
<button className="p-field__button -view" onClick = { () => {this.TurtleAttack(); this.setState({ShowFlag:true});}}>たたかう</button>
</div>
<div className="p-field__character-box -rabbit">
<HP CharacterHP = {RabbitHP} />
<img src={`${window.location.origin}/images/rabbit.png`} alt="キャラクターの画像" className="p-field__character -rabbit"/>
</div>
</div>
</div>
{ShowDamage}
</div>
);
}
まず、ShowDamage
について解説します。
let ShowDamage = this.state.ShowFlag ? <Comment Damage = {this.state.Damage} name = {this.state.name} /> : '';
ここはShowFlag
がtrueのときCommentコンポーネントを表示し、falseのときは何も表示しないという意味です。
これはReactチュートリアルにも出て来るので確認してみて下さい。
buttonをクリックするとShowFlag
をfalseからtrueに切り替えます。
この{ShowDamage}
をrender内に入れておくことでボタンを押すと表示されるコメント欄を実装できます。
以上でバトルフィールドの実装は終了です。
結果画面の実装
最後に結果画面を実装します。
以下のTurtle_win.tsx,Rabbit_win.tsxをscreens内に作成して下さい。
class TurtleWin extends React.Component {
render() {
return(
<div className="l-winner">
<div className="p-winner">
<p className="p-winner__text -view">カメイ・ウェザーの勝ち!!</p>
</div>
</div>
);
}
}
export default TurtleWin;
import * as React from "react";
class RabbitWin extends React.Component {
render() {
return(
<div className="l-winner">
<div className="p-winner">
<p className="p-winner__text -view">バニー・パッキャオ<br/>の勝ち!!</p>
</div>
</div>
);
}
}
export default RabbitWin;
この画面への遷移はFieldに実装してあります。
関数、TurtleAttack ()
,RabbitAttack ()
内の
location.href ="/Turtle_win"
location.href ="/Rabbit_win"
でキャラクターのHPが0になった時、結果画面に遷移します。
以上で完成です!
お疲れ様でした。
ここまでのディレクトリ構成を確認しておきましょう。
root
├── dist
│ ├── css
│ │ └── style.css
│ ├── images
│ │ ├── rabbit.png
│ │ └── turtle.png
│ ├── index.html
│ └── js
├── node_modules
├── package-lock.json
├── package.json
├── src
│ ├── main.tsx
│ └── screens
│ ├── Comment.tsx
│ ├── Field.tsx
│ ├── GameStart.tsx
│ ├── HP.tsx
│ ├── Rabbit_win.tsx
│ ├── Test.tsx
│ └── Turtle_win.tsx
├── tsconfig.json
└── webpack.config.js
確認できたらターミナルで以下のコマンドを実行して下さい。
npm start
これで正常に動けば完了です!
最後に
この記事は私のReact学習の復習もかねて作成しました。
今回作ったものは本当に基礎的なものかと思いますが、Reactを学びはじめた私にとってはよくわからないエラーがでたりして大変でした。
Reactを学び始めた人は高い確率で同じエラーが出たりするのではないかと思います。
そのような人たちにこの記事を参考にしていただければ幸いです。
もしこの記事でわからないことがあれば質問して下さい。
出来る限り答えたいと思います。
以上です。
最後までみて頂きありがとうございます。