REACTのチュートリアルについてのメモです
1. 開発環境を作る
公式チュートリアルに沿って進めていきます
https://ja.reactjs.org/tutorial/tutorial.html
ブラウザ上で開発するかローカルで開発するか
ブラウザ上で開発出来るスターターコードを使えば開発環境の整備は不要ですが、今回僕はローカルでやりました。
ローカル環境を作る
以下の手順でローカルのプロジェクトを作成してサーバーを起動します
1. Nodejsをインストールする
公式サイトからダウンロードしてインストールします
https://nodejs.org/ja/
2. Create React Appでプロジェクトフォルダを作る
適当なディレクトリで以下を実行します
npx create-react-app my-app
3. サーバーを起動する
上記を実行するとmy-appというディレクトリが作成されて、デフォルトで必要なファイルが生成されていますので、my-appに移動してnpm startすればサーバーが待機します。
cd my-app
npm start
http://localhost:3000/ を開くと以下のように表示される筈です
チュートリアルでは作成されたファイルを使用しないので/src/の中のファイルを全部消して作り直します
cd my-app
cd src
rm -f *
cd ..
cd my-app
cd src
del *
cd ..
2. Gameクラスを作成して表示する
チュートリアルでは以下のような3目並べのゲームボードを作成します
https://codepen.io/gaearon/pen/gWWZgR?editors=0010
これを実現するため、1マスに該当するSquareクラスを9個並べてBoardクラスにして、これをGameクラスのプロパティとしてもたせた上で、ReactDOM.render()に表示してもらいます。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
class Square extends React.Component {
render() {
return (
<button className="square">
{/* TODO */}
</button>
);
}
}
class Board extends React.Component {
renderSquare(i) {
return <Square />;
}
render() {
const status = 'Next player: X';
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
class Game extends React.Component {
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
// ========================================
ReactDOM.render(
<Game />,
document.getElementById('root')
);
body {
font: 14px "Century Gothic", Futura, sans-serif;
margin: 20px;
}
ol, ul {
padding-left: 30px;
}
.board-row:after {
clear: both;
content: "";
display: table;
}
.status {
margin-bottom: 10px;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.square:focus {
outline: none;
}
.kbd-navigation .square:focus {
background: #ddd;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
これで先程と同様にnpm startして http://localhost:3000/ にアクセスすると、以下のように表示される筈です。
3. クラスに値を持たせる/値を参照する
以下のようにしてやればSquareクラスに文字を表示できます
class Square extends React.Component {
render() {
return (
<button className="square">
{"a"}
</button>
);
}
}
classに持たせた値を参照して表示する場合には以下のようにします。Square.render()内でthis.props.valueを参照しているので、Squareクラスのprops(プロパティ)のvalueを参照していることになります。
class Square extends React.Component {
render() {
return (
<button className="square">
{this.props.value}
</button>
);
}
}
BoardクラスからSquareクラスを作る際に引数としてvalueプロパティを作るようにします。
class Board extends React.Component {
renderSquare(i) {
return <Square value={i} />;
}
}
Boardクラスの引数通りに0~8の番号を表示するようになりました
4. インタラクティブなコンポーネントを作る
buttonにonClick={}を追加することでSquareクラスのオブジェクトがクリックされたときにalertを出すようにしてみましょう。以下のように関数を渡してやることでクリックされたときにreactがalert()を出してくれます。
class Square extends React.Component {
render() {
return (
<button className="square" onClick={function() { alert('click'); }}>
{this.props.value}
</button>
);
}
}
なお、上記のようにfunctionが重なるとthisがどれを指しているのか分かりにくくなるので下のようにarrow関数で書く方が良いそうです。
class Square extends React.Component {
render() {
return (
<button className="square" onClick={() => alert('click')}>
{this.props.value}
</button>
);
}
}
5. クリックしたらXを表示するようにする
まず、コンストラクタ(クラスが最初に呼ばれた時に実行される関数)を追加して状態を記憶させるthis.stateを初期化するようにします。最初にsuper(props)としてるのは、ES2015(ES6)のJavaScriptクラスではsuper()クラスを呼ぶまでthisもsuperも使えなくなる仕様になっているからだそうで、コンストラクタを書くときは常に最初にsuper(props)するように推奨されています。
続いて、buttonに表示される値をthis.state.valueに変更し、onClickでthis.state.valueを'X'に変更するようにします。
class Square extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
}
render() {
return (
<button className="square" onClick={() => this.setState({value: 'X'})}>
{this.state.value}
</button>
);
}
}
6. React DevTools
ChromeまたはFirefoxの拡張機能として提供されているReact DevToolsを使うとReactアプリケーションのpropsとstateを確認できるようになります。
インストール手順は以下を参照してください
https://ja.reactjs.org/docs/optimizing-performance.html
拡張機能を有効にすると、ブラウザの開発者向けツールにreactのcomponentsタグとProfilerタグが追加されて、各クラスのプロパティが確認できるようになります。
7. 子コンポーネントの値を親コンポーネントに監理させる
ここまでのコードではSquareクラスが自分で表示するための値を保持していましたが、親クラスに監理させた方がコードが分かりやすく、より壊れにくく、リファクタリングしやすくなるそうですので、そのように書き換えていきます。
まず、親クラスにコンストラクタ関数を書いてBoard.state.squaresを初期化、この値を使ってSquareオブジェクトを表示してやります。また、先程までSquareクラスに書いてあったonClickで呼び出す関数もBoardクラスにhandleClick()として書いてやります。これにより値の監理をやりやすくなります。
また、handleClick()内でthis.state.squaresの値を一旦slice()してから書き換えているのはコードをイミュータブルにする為です。元の値を直接いじらないイミュータブルな書き方にする事で変更の有無を検出しやすくしたり、変更の履歴を保存したり、render()すべきタイミングを把握しやすくする効果が期待できるそうです。
class Board extends React.Component {
constructor(props){
super(props);
this.state = {
squares: Array(9).fill(null),
};
}
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = 'X';
this.setState({squares: squares});
}
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
Squareでやっていた値の監理がなくなったのでコンストラクタは消し、onClickでは親クラスから受け取ったSquare.props.onClick()関数(中身はBoard.handleClick())をonClickで実行するように書いてやります。
class Square extends React.Component {
render() {
return (
<button
className="square"
onClick={() => this.props.onClick()}
>
{this.props.value}
</button>
);
}
}
クリックするとBoardクラスにstateの値が変わり、それに合わせてXが表示されていくようになりました。
これによりSquaredはクリックされたことを親クラスに伝えるだけのコンポーネントになって、ロジックが書きやすくなりました。
8. 関数コンポーネント
上記の変更によりSquareクラスに値を持たなくなったのでクラスでなく関数で書いたほうが簡潔になります。{() => this.props.onClick()}の代わりにprops.onClickと書き換えるので、以下のようにかなり短く書くことができます。
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
9. 手番の処理
Xの手番の次はOにならないといけないので、次の手番がXOのどちらなのか書いてやります。BoardクラスにxIsNextプロパティを設定して、trueだったら次はXの手番、falseだったら次はOの手番として処理します。
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
class Board extends React.Component {
constructor(props){
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
};
}
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
10. 勝者を判定する
勝者を判定する関数を作成します。squaresは初期値がnullなのでif文からするとfalseと扱われるのを利用してnullではない値が何れかのlineすべてに入っていたら勝者は入っていた値であるという関数ですね。
function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
Board.render()内でcalculateWinner()を使って勝者判定を行い、勝者を表示するようにします。
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner){
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
また、勝者が決まった後に手番が進められるとおかしいので、Board.handleClick()内でcalculateWinner()がnullでないときは手番の更新を行わないようにします。
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
11. 履歴の保存
履歴を残しておいて手番を戻ることができるようにします。具体的には、以下のようなフォーマットで履歴を保存しておいて、戻れるようにしてやります。
history = [
// Before first move
{
squares: [
null, null, null,
null, null, null,
null, null, null,
]
},
// After first move
{
squares: [
null, null, null,
null, 'X', null,
null, null, null,
]
},
// After second move
{
squares: [
null, null, null,
null, 'X', null,
null, null, 'O',
]
},
// ...
]
上記のhistoryをGame.props.statesとして管理するように書き換えます。
handleClick()でhistoryに最新のsquaresを追加して上書きすることで1つずつ追加される挙動を作っています。
チュートリアルではこれに並行してBoardクラスで管理してた値をまとめてGameクラスで管理するように変更する作業もやってますので、以下のようにごっそり書き換えてしまってますが、自分でやるときはまず値の管理を置き換えて、正常に動いてから機能追加した方が良いと思います。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
render() {
return (
<div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
xIsNext: true,
}
}
handleClick(i) {
const history = this.state.history;
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares
}]),
xIsNext: !this.state.xIsNext,
});
}
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
let status;
if (winner){
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.props.xIsNext ? 'X' : 'O');
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
// ========================================
ReactDOM.render(
<Game />,
document.getElementById('root')
);
function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
12. 履歴の表示
とりあえず表示
ここでJavaScriptの標準機能である配列のmapメソッドを使います
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]
Game.render()内でhistoryの値からbutton表示させるhtmlを生成し、olタグ内に貼り付けることで戻るためのbuttonを作ります。
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
)
})
let status;
if (winner){
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.props.xIsNext ? 'X' : 'O');
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
この時点ではbuttonタグのonClick()で呼ばれるjumpTo()を書いていないのでbuttonを押すとエラー停止になります。
historyにkeyを追加
この時点で実行するとconsoleに以下のような警告がでます
リスト項目に固有のkeyが置かれていないので良くないですよ、と警告されています。順番だけで管理してると項目が増えた時に混乱するのでチュートリアルにも「動的なリストを構築する場合は正しい key を割り当てることが強く推奨されます」とありますから、素直に従ってkeyを追加してやります。
具体的には
{hoge(move)}を{hoge(move)}と書き換えます。reactがkey={move}を認識してそこに書かれたhoge(move)にGame.state.moveを渡せば良いんだと理解してくれます。 render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
)
})
jumpTo()を書く
戻るための関数を書いていきます。まずconstructorに現在表示している手番が何番目かを示すstepNumberを設定し、jumpTo()するとstepNumberとxIsNextをその時点での値に戻し、その後render()する際に表示する盤面を最新のものhistory[history.length - 1]ではなく指定されたものhistory[stepNumber]に変更、handleClick()されたら手戻りしたところまでのhistoryに追加して書き加えていくようにします。
jumpTo()の時点でhistoryを書き換えてしまうと履歴をウロウロすることが出来なくなっちゃいますので新たに手番が指されたときに更新するようにしてるわけですね。
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0,
xIsNext: true,
}
}
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares
}]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext,
});
}
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0,
});
}
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
)
})
ということで完成したコードがこちらです
https://codepen.io/gaearon/pen/gWWZgR?editors=0010
13. buildする
アプリケーションが完成しましたのでデプロイ用にbuildしましょう。
まず、相対パスで書けるようにpackage.jsonに"homepage": "./"を追記します。
{
"name": "my-app",
"version": "0.1.0",
"homepage": "./",
あとはプロジェクトフォルダでnpm run buildするだけです
cd my-app
npm run build
プロジェクトフォルダ内にbuildフォルダが生成されています
このindex.htmlをブラウザで開くとアプリケーションが表示されます
14. 次に何をやれば良いか
この先、ドキュメントはreact.jsの主なコンセプトの解説へと続いていきます。JSXやstateのライフサイクル、イベント処理やReactの流儀など重要な項目にフォーカスして説明しているので続いて読んでいくのが良さそうです。
https://ja.reactjs.org/docs/hello-world.html
- Hello World
- JSX の導入
- 要素のレンダー
- コンポーネントと props
- state とライフサイクル
- イベント処理
- 条件付きレンダー
- リストと key
- フォーム
- state のリフトアップ
- コンポジション vs 継承
- React の流儀
感想
チュートリアルをやる前にREACTは学習コストが高くて云々という記事も多くみかけたんですが、classベースで書くやり方も情報を上位クラスで保持する書き方もそこそこ複雑なコードを書く時の作業性に効いてきそうですし、デバッグがやりやすいようにブラウザ拡張ツールでエラーコードが確認できるのもありがたいです。少なくとも、生のJavaScriptよりは大幅にコードが書きやすいのでreact.jsを食わず嫌いしてるウェブ開発ビギナーはチュートリアルだけでもやってみてほしいです。