##概要
React/Reduxを少しかじったので,自分のメモがてら導入からreact-reduxの導入など,React/Reduxを使ったアプリケーションの雛形の作り方を書いていく.
準備
node.jsのインストール.ここでは以下のバージョンを前提に話を勧めていく
$ node -v
v9.11.1
npmでcreate-react-appのインストール
Reactを使うには.色々なライブラリ(e.g., babel)をインストールしなければならない.これはとても面倒なので,必要なライブラリのインストールや設定,アプリケーションの雛形を作ってくれる"create-react-app"をnpmでインストールする.
>npm install -g create-react-app
なお,-g
オプションはグローバルにインストールする.と意味になる.
雛形の作成と実行
create-react-app
によって簡単に雛形を作ることができる
>create-react-app hello
>cd hello
>npm start
この3行で."hello"というディレクトリにアプリケーションの雛形を作成し,実行する.npm start
を実行すると.勝手にブラウザがhttp://localhost:3000/
をアクセスし,Reactの雛形のアプリケーションが見えるようになる.
雛形の解説
helloディレクトリを見ると
>ls
>README.md node_modules/ package.json public/ src/ yarn.lock
このようなディレクトリ構成なっており,npm start
としたときに開かれるのはpublic/index.html
である.
index.htmlを見ると,
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
となっている.
続いて,src/index.js
をみると
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();
となっていて,document.getElementById('root')
という記述が確認できる.
これはつまり,index.html
の<div id='root'>
に<App/>
を描画する,ということを意味している.
そして,描画処理,およびJSX(後述)を利用するため,import ReactDOM from 'react-dom'
,import React from 'react'
でインポートする必要がある.
Reactコンポーネント
先のAppはReactコンポーネントと呼ばれる.Appはsrc/App.js
に定義されている.
もう少し詳しく言うと,App
はReactのクラスコンポーネントである(この他にFunctionコンポーネントがある).
クラスコンポーネント
クラスコンポーネントの例としてApp
を取り挙げる.
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<Hello/>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}
export default App;
クラスコンポーネントは以下の制約を守る必要がある.
- Componentを継承する
- render()メソッドを定義する
- renderでは,Reactコンポーネントを返す.
Appはこの制約に従っている.なお,この例ではrenderは<div>
を返しているが,この<div>
はHTMLではなくReactが予め用意してあるReactコンポーネントである.この例のように,Reactでは多くのHTMLタグに対応するコンポーネントが予め定義されている.
関数コンポーネント
先ほどのApp.jsに自作コンポーネントとして関数コンポーネントを追加する.
//(省略)
import './App.css';
const Hello = () => { // ここから関数コンポーネントの定義
return <p>Hello</p>;
}; // ここまで
class App extends Component {
render() {
return (
<div className="App">
//(省略)
<Hello/> // ここでコンポーネントを使う
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}
クラスコンポーネントに対して関数コンポーネントはシンプルで,Reactコンポーネントを返すという制約を守れば良い.先のHello
は,<p>
というReactコンポーネントを返している.
関数コンポーネントとクラスコンポーネントの使い分けは,stateを持つかどうかによって決めれば良い.
JSX
JSXは,簡単に言うとJavaScriptの中にHTMLのタグ(Reactコンポーネント)を記述できるようにする仕組み.
例えば,
return (
<div className="App">
...
);
はすでにJSXで,returnの返り値にdiv
タグ(に相当するReactコンポーネント)を返している.
さらに,Reactコンポーネントの中にもJavaScriptを記述することができる.
(略)
const f = () => { // 関数の定義
return "yahoo";
}
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<h1>{f()}</h1> // f()の実行と返り値の表示
<h/>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}
この例では,f
という関数を定義し,これをJSXの中で呼び出している.
このように,プログラムをJSXの中で実行するためには,{
と}
で囲む.かつ,中に書けるのは式だけである(文はかけない).文を書くにはクロージャ作って実行,のようにしないといけない.
Props
Reactコンポーネントは,外部からデータを受け取る仕組みとしてPropsという仕組みを持っている.
class MyComp extends Component {
render() {
return "Hello " + this.props.name ;
}
}
const MyfComp = (props) => {
return "Hello " + props.name;
}
この例のように,クラスコンポーネントの場合はthis.props.xxx
でアクセスし,関数コンポーネントの場合は引数にprops
を指定してprops.xxx
のようにアクセスすればよい.
これを使うには,
<MyComp name="hoge1"/>
<MyfComp name="hoge2"/>
こんな風に書けば良い.
Propsのチェック
ReactにはPropsの型チェックをする機能がある.これを使うことで,実行前に”渡されるべきプロパティが設定されない”,とか,"渡すプロパティの型違い"を検出して実行時エラーを減らすことができる.
そのためには,prop-typesパッケージをインストールする
>npm install --save prop-types
こうした上で,例えば上記のMyComp
コンポーネントのname
プロパティをstring
型としてチェックしたい場合,以下のように記述する.
(略)
import PropTypes from 'prop-types';
(略)
MyComp.propTypes = {
name: PropTypes.string,
};
こうした上で,例えば
<MyComp name={12}/>
のように書いてみると,JavaScriptのConsoleで以下のようなwarningが出る.
index.js:2178 Warning: Failed prop type: Invalid prop `name` of type `number` supplied to `MyComp`, expected `string`.
in MyComp (at App.js:34)
in App (at index.js:7)
なお,name=12
と書くことはできない.なぜなら,HTMLの原則に従わなければならないので,属性は””で囲まなくてはならない.そうすると,それらは文字列として扱われるので,例えばnumber
型などを使う場合にはJSXの中で{}を使ってJSのコードとして記述する必要がある.
また,isRequired
を指定することで,必須プロパティとすることも可能
MyComp.propTypes = {
name: PropTypes.string,
age: PropTypes.number.isRequired,
};
この場合,age
を指定しないとJavaScriptのコンソールにWarningが出る.
ここまでのエラーのないApp.jsのコードは次のようになる.
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import PropTypes from 'prop-types';
const Hello = () => {
return <p>Hello</p>;
};
const f = () => {
return "yahoo";
}
class MyComp extends Component {
render() {
return "Hello " + this.props.name ;
}
}
const MyfComp = (props) => {
return "Hello " + props.name;
}
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<h1>{f()}</h1>
<MyComp name="hoge1"/>
<MyfComp name="hoge2"/>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}
MyComp.propTypes = {
name: PropTypes.string,
/*age: PropTypes.number.isRequired,*/
};
export default App;
Propsのデフォルト値
Propsはデフォルト値を与えることができる.
MyComp.defaultProps = {
name:'abc'
};
これで,指定しない場合はprops.name
は"abc"となる.
イベントハンドラ
Reactコンポーネントはイベントハンドラを設定することができる.Appコンポーネントにbutton
コンポーネントを追加し,onClick
イベントを受け取ってみる.
(省略)
class App extends Component {
handleClick() { // イベントハンドラ
console.log("hello");
}
render() {
return (
<div className="App">
(省略)
<button onClick={this.handleClick}>Register</button> // buttonの定義とハンドラの設定
</div>
);
}
}
まずイベントハンドラhandleClick
関数を定義し,button
コンポーネントのonClick
イベントに関連付ける.これでボタンが押されるたびに,コンソールに"hello"と表示されるようになる.
イベントと値の変更
イベントハンドラでイベントを受け取ったら,それに応じて処理をし,画面を更新したいということがある.そこで,Appコンポーネントを次のように変更する
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import PropTypes from 'prop-types';
const Hello = () => {
return <p>Hello</p>;
};
const f = () => {
return "yahoo";
}
class MyComp extends Component {
render() {
return "Hello " + this.props.name ;
}
}
const MyfComp = (props) => {
return "Hello " + props.name;
}
class App extends Component {
constructor() {
super();
this.ary = [1,2,3,4,5];
this.handleClick = this.handleClick.bind(this); // (1)イベントハンドラのコンテキスト設定
}
handleClick() {
this.ary.push(6); // (2)イベント発生時にaryに追加
console.log(this.ary);
console.log("hello");
}
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<h1>{f()}</h1>
{
(() => { // (3)aryの個数に応じて"abc"を画面に表示
let list = [];
for (var i = 0; i < this.ary.length; ++i) {
list.push(<div key={i}>abc</div>);
}
console.log(list);
return <div>{list}</div>
})()
}
<MyComp name="hoge1"/>
<MyfComp name="hoge2"/>
<button onClick={this.handleClick}>Register</button>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}
MyComp.propTypes = {
name: PropTypes.string,
/*age: PropTypes.number.isRequired,*/
};
export default App;
変更後のコードでは,Appコンポーネントに配列(ary)を追加し,その個数分だけ"abc"という文字列を画面に表示するようにしている.主に重要なのは(1)~(3)になる.
(1)は,イベントハンドラの実行コンテキストを設定している.JavaScriptはfirst class functionなので,関数がオブジェクトになる.そのため,関数を引数で渡したり,返り値として返したりできる.この場合,クラスのメソッドを関数オブジェクトとした時,その関数の内部でthis
を使うとき,this
が指すオブジェクトが何なのかを決める必要がある.そのため,Reactではbind
メソッドによって実行時のthis
を設定する手段を提供している.(1)の場合.handleClick
が実行されるときのthis
(コンテキスト)をAppコンポーネントに固定している.そのため,handleClick
の中で参照するthis.ary
は,Appのconstructor
で設定するary
になる.
(2)では,handleClick
が実行された時,Appのconstructor
で設定したary
に要素を追加している.
(3)は,this.ary
の個数分だけ<div>abc</div>
コンポーネントを画面に表示するため,<div>abc</div>
コンポーネントを個数分だけlist
に追加し,最後に<div>{list}</div>
を返している.JSXの中で記述するJavaScriptは式でなければいけないので,文を書くことはできない.そのため,ここではクロージャを作成し,クロージャを実行してその返り値として<div>{list}</div>
を受け取り,画面に表示している.
さて,このコードを実行すると,"abc"という文字が5回表示される.そして,ボタンをクリックするとary
に要素が追加されるので,"abc"は6回表示されることを期待する.しかしそうはならない.なぜならば,ary
に6が追加されても,そのことを通知し,クロージャを再実行する必要があるからで,ここではそんな処理をしていないからである(console.logでのthis.ary
の出力を見ると6が確かにに追加されていることは確認できる).アプリケーションが複雑になるとこのイベント通知はカオスになる.これに対する解決策を与えるのがReactなのだと個人的には考えている.Reactは後述するステートを利用することで,この更新処理を開発者から隠蔽する.
ステート
Reactのクラスコンポーネントはステートを持ち,ステートが変更されたときに必要なコンポーネントだけが再描画するような仕組みになっている(どうやっているのか今はブラックボックス).先のコードをステートを使うように変更する.
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import PropTypes from 'prop-types';
const Hello = () => {
return <p>Hello</p>;
};
const f = () => {
return "yahoo";
}
class MyComp extends Component {
render() {
return "Hello " + this.props.name ;
}
}
const MyfComp = (props) => {
return "Hello " + props.name;
}
class App extends Component {
constructor() {
super();
//this.ary = [1,2,3,4,5]; // 削除
this.state = { // (1)ステートの生成
ary: [1,2,3,4,5]
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// this.ary.push(6);
const { ary } = this.state; // (2)ステートからaryの取り出し
ary.push(6); // 値の挿入
this.setState({ary}); // (2)ステートの更新
console.log("hello");
}
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<h1>{f()}</h1>
{
(() => {
let list = [];
const {ary} = this.state; // (3)ステートからaryの取り出し
for (var i = 0; i < ary.length; ++i) { // (3) 取り出したaryを使ってオブジェクトを生成
list.push(<div key={i}>abc</div>);
}
console.log(list);
return <div>{list}</div>
})()
}
<MyComp name="hoge1"/>
<MyfComp name="hoge2"/>
<button onClick={this.handleClick}>Register</button>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}
MyComp.propTypes = {
name: PropTypes.string,
/*age: PropTypes.number.isRequired,*/
};
export default App;
主な変更点は3つ.
(1)まず,ステートはJavaScriptのオブジェクトで,1つのクラスコンポーネントに1つ存在する.そして,ステートの作成はコンストラクタで行う.ここでは,
this.state = {
ary: [1,2,3,4,5]
};
このように,オブジェクトのメンバとしてary
,その値に[1,2,3,4,5]
を設定してthis.state
にオブジェクトを設定する.これがステートの初期化となる.
(2)ステートから必要なメンバだけを取り出し,メンバに値を追加した後,ステートに再設定している.多くのReactの解説にあるように,ステートの中身に直接値を代入すると,正しく再描画されないことがある.具体的には,以下のように書いてはならない
this.state.ary.push(6);
こう書くと,Reactコンポーネントがステートが変化したかどうか判定できない可能性があるためである.変更を正しく伝えるには,this.setState
メソッドを利用して新しいステートオブジェクトを再設定する.ここでは,取り出したary
を使って,新しいステートオブジェクトを生成し,それを新たなステートとして再設定している.このsetState
メソッドはComponent
クラスに定義されている(のだろう).
(3)ステートから取り出したary
を使って<div>abc</div>
オブジェクトを必要な数だけ生成している.
以上の変更により,(2)のthis.setState
でステートの変更がReactに通知され,ステートを参照しているオブジェクトに変更が通知されることにより再描画されるようになる.
とりあえず,Reactを使った最低限のアプリケーションができた.が,これはまだアプリケーションとは呼べない代物だし,ソースコードも汚い.よって,ファイルを分割するなどしてもうちょっと意味のあるアプリケーションにしていく.次回に続く..