Reactチュートリアルやってみたその2
前回は公式チュートリアルでしたが、今回はReact Tutorial: An Overview and Walkthroughというサイトのチュートリアルで各ファイルに機能を分け、中枢に当たるファイルでそれらをインポートとするようなReactアプリの作り方とnpmによるGitHub Pageに作ったReactアプリをデプロイするやり方を見ていきます。
実は、公式チュートリアルは最後に追加課題があったのですが私は知らずに飛ばしてしまったのでこちらを今回はやっています。
ここまで、やるとおそらく簡単なReactアプリを作成し、それをデプロイするまで(ただし、今回までの範囲ではサーバーを用意していないのでローカル環境のみ)のスキルが身につくはずです。
今回やること
冒頭でも書いた通りですが、前回でReactの仕組みやアプリの作り方の基本的な考え方や作り方を公式チュートリアルで学んだので、今回は少し応用として
- コンポーネントに1つのファイルから各ファイルに分割する
- npmでビルドする
- GitHub Pagesにデプロイする
という以下の3点に絞って進めていきます。
コンポーネントの分割
今回は以下のようなディレクトリ構造で作っていきます
具体的にはindex.js
にApp.js
をインポートし、さらにApp.js
に各コンポーネントをインポートするような形でアプリを作っていきます。
フレームワークを使ったことがある人ならわかると思いますが、前回のように1つのファイルにすべての処理を記述するようなやり方は可読性やメンテナンス性の悪さから推奨されないので、例えばDjangoであるなら1つのアプリケーションを作るにしても機能ごとにフォルダを作り、さらにその中でもコンポーネントに分割したりととかく最小単位を突き詰めていくような作り方をします。
最小単位を突き詰めれば、必然的に1つのファイルに書かれるコードは短くなり、結果どのファイルにどの処理が書いてあるかということをわかりやすくするということにつながるわけですね。
閑話休題、では今回の各ファイルを見ていきましょう。
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App'
import './index.css'
// 最後に、React DOM render() メソッドを使って、作成した App クラスを HTML のルート div にレンダリングします。
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
App.js
import React, { Component } from 'react'
import Table from './Table'
import Form from './Form'
class App extends Component {
state = {
characters: [
],
}
removeCharacter = (index) => {
const { characters } = this.state
this.setState({
characters: characters.filter((character, i ) => {
return i !== index
}),
})
}
handleSubmit = (character) => {
this.setState({characters: [...this.state.characters, character]})
}
render() {
const { characters } = this.state
return (
<div className="container">
<Table characterData={characters} removeCharacter={this.removeCharacter} />
<Form handleSubmit={this.handleSubmit} />
</div>
)
}
}
export default App
Form.js
import React, {Component} from 'react'
class Form extends Component {
initialState = {
name:'',
job:'',
}
state = this.initialState
handleChange = (event) => {
const {name, value} = event.target
this.setState({
[name]: value,
})
}
submitForm = () => {
this.props.handleSubmit(this.state)
this.setState(this.initialState)
}
render() {
const { name, job } = this.state;
return (
<form>
<label htmlFor="name">Name</label>
<input
type="text"
name="name"
id="name"
value={name}
onChange={this.handleChange} />
<label htmlFor="job">Job</label>
<input
type="text"
name="job"
id="job"
value={job}
onChange={this.handleChange} />
<input type="button" value="Submit" onClick={this.submitForm} />
</form>
);
}
}
export default Form;
Table.js
import React from 'react'
// シンプルコンポーネント
const TableHeader = () => {
return (
<thead>
<tr>
<th>Name</th>
<th>Job</th>
<th>Remove</th>
</tr>
</thead>
)
}
const TableBody = (props) => {
const rows = props.characterData.map((row, index) => {
return (
<tr key={index}>
<td>{row.name}</td>
<td>{row.job}</td>
<td>
<button onClick={() => props.removeCharacter(index)}>Delete</button>
</td>
</tr>
)
})
return <tbody>{rows}</tbody>
}
const Table = (props) => {
const {characterData, removeCharacter} = props
return (
<table>
<TableHeader />
<TableBody characterData={characterData} removeCharacter={removeCharacter} />
</table>
)
}
// クラスコンポーネント
// class Table extends Component {
// render() {
// return (
// <table>
// <thead>
// <tr>
// <th>Name</th>
// <th>Job</th>
// </tr>
// </thead>
// <tbody>
// <tr>
// <td>Charlie</td>
// <td>Janitor</td>
// </tr>
// <tr>
// <td>Mac</td>
// <td>Bouncer</td>
// </tr>
// <tr>
// <td>Dee</td>
// <td>Aspiring actress</td>
// </tr>
// <tr>
// <td>Dennis</td>
// <td>Bartender</td>
// </tr>
// </tbody>
// </table>
// )
// }
// }
export default Table
Api.js
Api.js
については最後に説明していきます。
なお、CSSについては進行上コピペで対応してくださいとのことです。
各ファイルの各コードについてはリポジトリの方に私がわかりにくかったところについて少し注釈を入れてあります。
各ファイルの役割を見ていきましょう。
今回は単純にフォームに入力されたデータをテーブルで表示して、削除もできるようにするということができるように作業を進めていきます。
index.js
は最終的なrender()
を行うためのファイルです。
今回はApp.js
にアプリの機能を集約させるのでそちらのAppクラスをrender()
という役割を持っています。
そのApp.js
は機能の中枢であるので各コンポーネントからデータを受け取ったり、必要に応じて各コンポーネントへデータを渡したりします。
Table.js
はApp.js
からデータを受け取り、受け取ったデータをpropsに格納し、それを用いてテーブルを描画する役割を持っています。
今回、Table.js
はstateを持たないので関数コンポーネントのみで構成されています。
Form.js
は新たにデータを追加するための役割を持ちます、より具体的にはフォーム内のフィールドが変更されるたびにローカルのフォームの状態を更新し、送信時にはそのデータがすべてAppの状態に渡され、テーブルが更新されるような処理を担っています。
つまり、フォームのフィールドに応じてstate(プロパティ)が変更されますが、実際のプロパティとして確定するのは送信時のプロパティということになります。
よって、処理の流れとしては以下の通りとなります。
-
Forms.js
(データの入力) -
App.js
(Form.jsからデータを受け取り、Table.jsパスする) -
Table.js
(テーブルを描画する)
正確にはApp.js
にはTable.js
、Forms.js
それぞれからForm・TableコンポーネントをインポートしているのでApp.js
ですべて行われていることになりますが、わかりやすくいうと上記のような流れになります。
今回のミソはTable.js
、Forms.js
のような子コンポーネントでも複数のコンポーネントがありますが、最終的にその中でも1つのコンポーネントへデータをパスし、そのコンポーネントを親コンポーネントにインポートしているというところになりますね。
例えばTable.js
なんかはTableHeader
及びTableBody
コンポーネントでマークアップしている部分がありますが、それらはすべてreturnとして返り値にされTable
コンポーネントへ渡されています。
さらにTable
コンポーネントでそれらは再度返り値にされることでインポート先のApp.js
へ渡され、そこで描画の処理が行われるという流れになります。
もちろん最終的な描画の処理はindex.js
で行われるわけですが、一見複雑かつ煩雑に見えるこの流れも丁寧に追っていけば理にかなっていることがわかりますね。
インポートに関してはimport App from './App'
やimport './index.css'
といったように書いていきます。
同時にインポートさせたいコンポーネントはexport default ~
とコードの末尾に書いて置かなければいけません。
例えばTable.js
のTableコンポーネントをインポートしたい場合は、Table.js
側でexport default Table
と定義をしておかないといけないということですね。
export default ~
は1ファイルで1コンポーネントのみにしか使えません。
最後にApi.js
ですがこれはReactにおいてAPIを使ったアプリケーションを作るための簡単なチュートリアルと思ってください。
今回はWikipediaのAPIを利用して、そのAPIデータをDOMにレンダリングしています。
Api.js
とApp.js
の切り替えはindex.js
においてインポートをどちらにするかで決まります。
ビルドとデプロイ
JavaScriptやCSSは実際のページなどをDeveloper Toolなどでみると
/*! For license information pease see 2.f0b459d6.chunk.js.LICENSE.txt */
(this.webpackJsonptutorial2=this.webpackJsonptutorial2||[]).push([[2],[function(e,t,n){"use strict";e.exports=n(10)},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}n.d(t,"a",(function(){return r}))},function(e,t,n){"use strict";function r(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function l(e,t,n){return t&&r(e.prototype,t),n&&r(e,n),e}n.d(t,"a",(function(){return l}))},function(e,t,n){"use strict";function r(e){return(r=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.
....
といったような状態で表示されることがよくあります。
これは本番環境においてはソースコード(これまで書いてきたコード)を生のままおかないということと、ReactのようなTypeScriptを使う場合はこのような形式に変換しないとブラウザ上では動作しないからです。
ソースコードをこのような形式に変換することをビルドというそうです。
React……というより、TypeScriptを使う場合はnpmの利用が不可欠になってくるので必然的にビルドはnpmが行うということになります。
といってもそんなに難しいことではなくpackage.json
があるディレクトリで
npm run build
を実行すればいいだけです。
ただ、今回はGitHub Pagesへのデプロイも行いたいので実行は後回しにします。
GitHub Pagesへのデプロイ
いよいよ最後の工程になります。
package.jsonを以下のように編集します
package.json
{
"name": "tutorial2",
"version": "0.1.0",
"private": true,
"homepage": "https://GitHubのユーザー名.github.io/リポジトリ名",
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"gh-pages": "^3.1.0"
}
}
編集が終わったらnpmにgh-pages
をインポートします。
このライブラリを使うことでいい感じにReactがビルドされてGitHub Pagesへとデプロイされます。
npm install --save-dev gh-pages
あとは先程のコマンドを用いてビルドしてデプロイするだけです。
npm run build
npm run deploy
注意事項としては今回はサーバーを用意していないので実際にデプロイした結果を画面で確認したい場合はyarn start
などでローカルサーバーを起動しないといけないことですね。
感想
Reactチュートリアルを2つ完走した感想ですが、やっぱりJavascriptは難しい
……というのは冗談半分としてJavascriptのフレームワークという以上にできることが多いなという印象でした。
無知ゆえに、どうしてもJavaScriptには動的な描画というところにだけ意識が行ってしまうのですが、特に今回のフォームの件はDBへの保存がないだけで、やっていることはデータの追加・削除とまるでサーバーサイドでやるようなことだったのでフロントでここまでやるのかという驚きがすごいです。
だってこれおそらくAjaxでDBへ渡してしまえばサーバーサイド側でやることは受け取ったJSONデータを変換してDBへ保存したり、検索をかけて合致したものを削除するような処理を書けばいいってことになるわけで……うーん、すごい……