React.js + ES6 + WebPack
のチュートリアルになります。
私は普段のプロジェクトでは
AngularJS 1.4
を使用しており
ReactJSを知ったかぶり でこれまで周りを騙してきましたが
そろそろ本当にヤバイ感じなので、ちゃんと勉強してみました
元ネタのLearnCode.academyさんのビデオはこちら
本記事では説明のため、かなり簡略化してます。
オリジナルのビデオ
https://www.youtube.com/watch?v=MhkGQAoc7bc
オリジナルのソースコード
https://github.com/learncodeacademy/react-js-tutorials
彼のYouTubeチャンネルには、他にも良いビデオがありますので是非ご覧ください。
Chap 1 セットアップ
1. ソースコードの作成
以下のファイルを作成します。
package.json
webpack.config.js
src/index.html
src/js/client.js
{
"name": "react-tutorials",
"version": "0.0.0",
"description": "",
"main": "webpack.config.js",
"dependencies": {
"babel-loader": "^6.2.4",
"babel-plugin-add-module-exports": "^0.1.2",
"babel-plugin-react-html-attrs": "^2.0.0",
"babel-plugin-transform-class-properties": "^6.3.13",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-preset-es2015": "^6.9.0",
"babel-preset-react": "^6.5.0",
"babel-preset-stage-0": "^6.5.0",
"react": "^15.1.0",
"react-dom": "^15.1.0",
"webpack": "^1.13.1",
"webpack-dev-server": "^1.14.1"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
var debug = process.env.NODE_ENV !== "production";
var webpack = require('webpack');
var path = require('path');
module.exports = {
context: path.join(__dirname, "src"),
devtool: debug ? "inline-sourcemap" : null,
entry: "./js/client.js",
module: {
loaders: [
{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel-loader',
query: {
presets: ['react', 'es2015', 'stage-0'],
plugins: ['react-html-attrs', 'transform-class-properties', 'transform-decorators-legacy'],
}
}
]
},
output: {
path: __dirname + "/src/",
filename: "client.min.js"
},
plugins: debug ? [] : [
new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
],
};
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>React Tutorials</title>
<!-- change this up! http://www.bootstrapcdn.com/bootswatch/ -->
<link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.6/cosmo/bootstrap.min.css" type="text/css"
rel="stylesheet"/>
</head>
<body>
<div id="app"></div>
<script src="client.min.js"></script>
</body>
</html>
import React from "react";
import ReactDOM from "react-dom";
class Layout extends React.Component {
render() {
return (
<h1>It works!</h1>
);
}
}
const app = document.getElementById('app');
ReactDOM.render(<Layout/>, app);
2. セットアップ
$ npm install
$ webpack --watch
src/client.min.js
が生成されます。(このファイルに全てのJSがパッケージされます。少し時間がかかります。)
この時点で下記のフォルダ構成になってるはずです。(node_modules
除いてます。)
~/W/myreact1 ❯❯❯ tree -I 'node_modules' . master ✱ ◼
.
├── package.json
├── src
│ ├── client.min.js // WebPackが自動生成します。
│ ├── index.html
│ └── js
│ └── client.js
└── webpack.config.js
2 directories, 5 files
3. 実行
ブラウザで index.html
を開きます。
$ open index.html
It works!
と表示されればOKです。
適当にclient.js
を書き換えてみましょう。
WebPackが更新を検出して再コンパイルしてくれます。
WebPackのコンパイルが遅いので、webpack-dev-server
を使用しましょう。
package.json を以下のように修正します。
"scripts": {
"dev": "webpack-dev-server --content-base src --inline --hot",
"test": "echo \"Error: no test specified\" && exit 1"
},
webサーバを起動します。
$ npm run dev
webでアクセスしましょう。
http://localhost:8080/index.html
client.js
ファイルを書き換えてみましょう。(勝手に更新してくれます。)
さっきよりかなり速くなります (差分だけ見てコンパイルしてくれるようです。)
Chap 2 コンポーネント
先ほどのclient.js
のLayout
コンポーネントをいじってみましょう。
クラス内にファンクションgetVal()
を実装します。
JSXから{this.getVal()}
で呼び出します。
class Layout extends React.Component {
getVal() { // 実装します。
return "will";
}
render() {
return (
<h1>It {this.getVal()} works!</h1> // JSX {...}でJavascriptを書きます。ハイライトがおかしい。。。 ><
);
}
}
const app = document.getElementById('app');
ReactDOM.render(<Layout/>, app);
実行します。It will works!
と表示されます。
次は、コンストラクターconstructor()
を実装して、this.val
を定義します。
class Layout extends React.Component {
constructor() { // コンストラクター
super();
this.val = "will"; // ここ
}
render() {
return (
<h1>It {this.val} works!</h1>
);
}
}
const app = document.getElementById('app');
ReactDOM.render(<Layout/>, app);
実行します。先ほどと同じ結果になります。
Chap 3 複数コンポーネント
先ほどのLayout
コンポーネントのコードを別ファイルcomponents/Layout.js
に切り出します。
export default
を付けると、他のファイルからビジブルになります。
import React from "react";
export default class Layout extends React.Component {
constructor() {
super();
this.val = "will";
}
render() {
return (
<h1>It {this.val} works!</h1>
);
}
}
client.js
を修正します。
インラインの定義を削除して、代わりにLayout
コンポーネントをインポートします。
import React from "react";
import ReactDOM from "react-dom";
import Layout from "./components/Layout"; // ここ
const app = document.getElementById('app');
ReactDOM.render(<Layout/>, app);
実行します。変更前と同じ結果になると思います。
次に新たにHeader
コンポーネントを作ります。
新規にcomponents/Header.js
を下記のように作成します。
import React from "react";
export default class Header extends React.Component {
render() {
return (
<h1>I am Header.</h1>
);
}
}
Layout.js
を修正し、Header
コンポーネントをインポートします。
つまり、Layout
が親コンポーネント、Header
が子コンポーネントになります。
import React from "react";
import Header from "./Header";
export default class Layout extends React.Component {
render() {
return (
<Header/>
);
}
}
実行します。I am Header.
と表示されます。
次に、Header
コンポーネントを3つ、リストに入れて表示してみます。
return
で返すトップのDOMエレメントは一つである必要があるためdiv
で囲まないとエラーになります。
import React from "react";
import Header from "./Header";
export default class Layout extends React.Component {
render () {
var list = [
<Header/>,
<Header/>,
<Header/>
];
return (
<div>
{list}
</div>
);
}
}
実行します。(I am Header.が3つ表示されます。)
Chap 4 ステートとプロップ
ステートstate
を使用してみましょう。
ステートはコンストラクターで定義します。
その名の通り、コンポーネントの状態を保持する、書き換え可能な値です。
Layout.js
を書き換えます。
import React from "react";
export default class Layout extends React.Component {
constructor() {
super();
this.state = {name1: "Will"}; // ここ
}
render () {
return (
<div>
<h1>{this.state.name1}</h1> // 参照します。
</div>
);
}
}
実行します。Will
と表示されます。(どうでもいいですが、ここではWill
は人名ですね。 )
次にsetTimeout()
を使用して、ステートの値を変更してみましょう。
...
render () {
setTimeout(() => {
this.setState({name1: "Bob"}) // 1秒後にBobに変更
}, 1000);
return (
<div>
<h1>{this.state.name1}</h1> // リアルなDOM?もちゃんと更新されます
</div>
);
}
1秒後にWillがBobに変わります。
次にプロップを使用してみましょう。
Layout
コンポーネントから、Header
コンポーネントのプロップtitle1
にインジェクトします。
import React from "react";
import Header from "./Header";
export default class Layout extends React.Component {
render () {
const title = "Welcome Will!";
return (
<div>
<Header title1={title} /> // インジェクト
</div>
);
}
}
Header
コンポーネントでconsole.log()
で確認しましょう。
import React from "react";
export default class Header extends React.Component {
render() {
console.log(this.props); // 出力: Object {title1: "Welcome Will!"}
return (
<h1>I am Header.</h1>
);
}
}
実行します。コンソールにObject {title1: "Welcome Will!"}
が出力されますね。
Chap 5 イベント
Header
コンポーネントにテキストフィールドを追加します。
やりたいことは、テキストフィールドにタイプした文字列を、リアルタイムに上の部分に表示する よくあるやつですね。
まずは手始めに仕組みづくりをします。
Header.js
を以下のように修正します。
import React from "react";
export default class Header extends React.Component {
render() {
return (
<div>
<h1>{this.props.title1}</h1> //テキストフィールドの値を、動的にここに表示したい
<input />
</div>
);
}
}
Layout.js
を以下のように修正します。
import React from "react";
import Header from "./Header";
export default class Layout extends React.Component {
constructor() {
super();
this.state = {
theTitle: "Welcome", // コンポーネントの状態を保たせます。
};
}
render () {
setTimeout( () => {
this.setState({theTitle: "Welcome Will!"}); // 3秒後に、状態を変化させます。
}, 3000);
return (
<div>
<Header title1={this.state.theTitle} /> // Headerコンポーネントにインジェクトします。
</div>
);
}
}
実行します。3秒後に、表示が変わりましたでしょうか?
もちろん、この段階ではテキストフィールドに値を入力しても何も変わりません。
完成形
- 親の
Layout
コンポーネントに、状態theTitle
を変更するインターフェースchangeTitleInterface(title)
を実装します。 - Headerコンポーネントのプロップ
callback1
に、インターフェースを渡します。子コンポーネントHeader
にコールバックさせるわけです。
(bindでthisを親コンポーネントに束縛します。)
import React from "react";
import Header from "./Header";
export default class Layout extends React.Component {
constructor() {
super();
this.state = {
theTitle: "Welcome",
};
}
changeTitleInterface(title) {
this.setState({theTitle: title})
}
render () {
return (
<div>
<Header title1={this.state.theTitle} callback1={this.changeTitleInterface.bind(this)} />
</div>
);
}
}
Header
コンポーネントを下記のように修正します。
import React from "react";
export default class Header extends React.Component {
handleChange(e) {
const title = e.target.value;
this.props.callback1(title); // 親のインターフェースをコールバック
}
render() {
return (
<div>
<h1>{this.props.title1}</h1>
<input onChange={this.handleChange.bind(this)} /> // ココは普通にonChangeイベントをフックします。
</div>
);
}
}
onChange
イベントをフックして、親のインターフェースをコールバックします。
そのさい、テキストフィールドの値を渡してあげるわけです。
すると、
- 親の状態
theTitle
が更新される -
Header
のプロップthis.props.title1
も変わる
状態自体はあくまで、親コンポーネントが管理し、子供はステートレスな作りにしておきます。
Chap 6 ルーティング
ルーティングを実装してみましょう。
まず、react-router
とhistory
をインストールします。
$ npm -S install react-router
$ npm -S install history@1
React Router
を使用して、タブバーを実装してみましょう。
3つのリンクを配置し、クリックしたリンクに応じてページの一部を更新します。
親ページとなるLayout
コンポーネントと、子ページとなるPage1
,Page2
,Page3
,コンポーネントを作成します。
client.js
を下記のように修正します。
import React from "react";
import ReactDOM from "react-dom";
import {Router, Route, IndexRoute, hashHistory} from "react-router"
import Layout from "./components/Layout"; // 親ページ
import Page1 from "./components/Page1"; // 子ページを3つインポート
import Page2 from "./components/Page2";
import Page3 from "./components/Page3";
const app = document.getElementById('app');
ReactDOM.render(
<Router history={hashHistory}>
<Route path="/" component={Layout}> // テンプレートとなる親ページ
<IndexRoute component={Page1}></IndexRoute> // 子ページ1。デフォルトページ
<Route path="page2" component={Page2}></Route> // 子ページ2
<Route path="page3" component={Page3}></Route> // 子ページ3
</Route>
</Router>,
app);
import {XXX} from "YYY"
export default
のdefaultが付いていないメンバをインポートする場合は、カーリーで囲みます。
/page2
にアクセスすると、Page2
コンポーネントがLayout
コンポーネントに読み込まれます。
親ページとなるLayout
コンポーネントを下記のように修正します。
this.props.children
に、選択したPage
コンポーネントがセットされます。
import React from "react";
import {Link} from "react-router" // リンクを作成するコンポーネント
export default class Layout extends React.Component {
render () {
return (
<div>
<h1>Layout</h1>
{this.props.children} // ここに選択した子コンポーネントが読み込まれます。
<Link to="/">Show Page1</Link> // リンクを3つ作成します。
<Link to="page2">Show Page2</Link>
<Link to="page3">Show Page3</Link>
</div>
);
}
}
最後に子ページのファイル3つを作成します。
import React from "react";
export default class Page1 extends React.Component {
render () {
return (
<h1>Page1</h1> // 同様にPage2, Page3を作成します。
);
}
}
URLを見ると、SPAの特徴であるハッシュ#
でルーティングしているのがわかります。