項目が多いので React tutorial example series に分けました。
モジュールのバーッションをアップデートして、内容も少しアップグレードしています。
- React Tutorial Example (Hot Module Replacement)
- React Tutorial Example (Redux)
- React Tutorial Example (ECMAScript 2015)
- React Tutorial Example (Convention)
- React Tutorial Example (PostCSS)
- React Tutorial Example (Package)
- React Tutorial Example (Jest)
- React Tutorial Example (Electron)
こちらはバージョンが古い記事です。
React は ユーザインターフェース を作成する JavaScript のライブラリです。
Virtual DOM や Data Flow が特徴で主に Facebook で開発されています。
v0.14
から React と ReactDOM の2つのパッケージに分割されました。
React Tutorial も ReactDOM のアップデートがされているので Redux や Socket.IO を利用してサンプルを実装しました。
実装した React Tutorial Example には6つの Tag でコードの差分を見ることができます。
- v0.0.1: React Tutorial
- v0.0.2: Webpack
- v0.0.3: Redux Tutorial 1st
- v0.0.4: Redux Tutorial 2nd
- v0.0.5: WebSocket
- v0.0.6: PropTypes
Environment
環境は React v0.14
がリリースされた時点の最新のバージョンで開発しています。
- node: v4.1.2
- npm: v2.14.4
- express: v4.13.1
- react: v0.14.0
- redux: v3.0.2
- socket.io: v1.3.7
Express
Webサーバーには Express を利用しました。
Express Generator で簡単にWebサーバーが構築できます。
$ npm install express-generator -g
$ express react-tutorial-example
$ cd react-tutorial-example && npm install
$ DEBUG=react-tutorial-example:* npm start
ブラウザで http://localhost:3000/
にアクセスしましょう。
Welcome to Express が表示されたら OK です。
次に進みましょう。
Git
SCM は Tutorial の進みの差分を見直すことが出来て便利です。
今回は 20 程度の Commit で実装しました。
$ echo node_modules/ > .gitignore
$ git init
$ git add --all
$ git commit -m "Initial commit"
特に Git は Tutorial に必要ありません。
React
Tutorial
React で実際に Comments Box を作成しながら Virtual DOM や Data Flow を体験しましょう。
(こちらに Tutorial の Source があります。)
Style
- まずは Express Generator で作成した Express の Stylesheet を Tutorial の Source からコピーします。
$ curl https://raw.githubusercontent.com/reactjs/react-tutorial/master/public/css/base.css -o ./public/stylesheets/style.css
Layout
- Express の Layout も Tutorial の Source からコピーしたい所ですが、テンプレートに Jade を利用しているのでHTMLを変換しました。
-
marked.min.js
は Tutorial の Adding Markdown で利用しますが、ここで追加しています。
views/layout.jade
:
doctype html
html
head
meta(charset='utf-8')
title React Tutorial
link(rel='stylesheet', href='/stylesheets/style.css')
script(src='https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react.js')
script(src='https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react-dom.js')
script(src='https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js')
script(src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js')
script(src='https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js')
body
block content
-
Express ではトップページのレイアウトを
index.jade
に記述します。 - しかし、 React は Virtual DOM なのでレイアウトに ID (
#content
) 以外のコードを記述しません。
views/index.jade
:
extends layout
block content
#content
script(type='text/babel', src='/javascripts/example.js')
script(type='text/babel').
Script
-
Tutorial の
tutorial1.js
からtutorial20.js
をexample.js
に記述しながら進みます。
public/javascripts/example.js
:
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
Hello, world! I am a CommentBox.
</div>
);
}
});
ReactDOM.render(
<CommentBox />,
document.getElementById('content')
);
それでは Tutorial を始めましょう。
Tutorial 1
- your-first-component では CommentBox の Component が追加されます。
いつもの Hello, world! で始まります。
Tutorial 2,3
- composing-components では CommentList と CommentForm が構造で追加されます。
ふたつの Hello, world! が表示されました。こうして Component ごとに分割してコードを記述します。
Tutorial 4,5
-
using-props では CommentList に Comment が構造で追加されて、
props
で値が表示されています。
データは上位の Component から下位の Component に流れます。
Tutorial 6,7
- adding-markdown では Comment が Markdown に変換されます。
another
が another に変換されていますね。
Tutorial 8,9,10
- hook-up-the-data-model では CommentList にデータモデルから値が渡されて表示されています。
表示は変わっていませんが、性能が大きく向上しました。
Search Comments API
次の Tutorial から サーバーに API が必要なので
Express に Comments
を取得する API を実装します。
-
Express の便利な Router で
Comments
を実装します。
app.js
:
var comments = require('./routes/api/comments');
app.use('/api/comments', comments);
- ファイルを読み込んで、レスポンスを作成します。
routes/api/comments.js
:
var fs = require('fs');
var express = require('express');
var router = express.Router();
/* GET comments listing. */
router.get('/', function(req, res, next) {
fs.readFile('db/comments.json', function(err, data) {
res.setHeader('Cache-Control', 'no-cache');
res.json(JSON.parse(data));
});
});
module.exports = router;
- データベースもどきは JSON にします。
db/comments.json
:
[
{"author": "Pete Hunt", "text": "This is one comment"},
{"author": "Jordan Walke", "text": "This is *another* comment"}
]
JavaScript Only は素敵!
Tutorial 11 - 14
-
fetching-from-the-server では CommentList に
GET /api/comments
から値が取得されて表示されます。
db/comments.json
の値を変更すると数秒後に値の表示が変わります。
Create Comment API
今度は Express に Comment を登録する API を実装します。
- 先ほどの Router に
router.post
を追加するだけで完成です。
routes/api/comments.js
:
/* POST comment creating. */
router.post('/', function(req, res) {
fs.readFile('db/comments.json', function(err, data) {
var comments = JSON.parse(data);
comments.push(req.body);
fs.writeFile('db/comments.json', JSON.stringify(comments, null, 4), function(err) {
res.setHeader('Cache-Control', 'no-cache');
res.json(comments);
});
});
});
Tutorial 15 - 20
- adding-new-comments では CommentForm から Comment を Post します。
送信するとデータベースもどきに author
と text
が保存されます。
これで React の Comments Box が完成しました。
Webpack
Webpack は依存関係のモジュールを取込み、静的な Assets を生成するライブラリです。
今回は先ほどの Tutorial で利用したモジュールを Webpack から生成しましょう。
$ npm install webpack --save-dev
$ npm install webpack-dev-middleware --save-dev
$ npm install babel-loader --save-dev
$ npm install jquery --save
$ npm install react --save
$ npm install react-dom --save
- Express に Webpack の機能を追加します。
app.js
:
var webpack = require('webpack');
var webpackDevMiddleware = require('webpack-dev-middleware');
var webpackConfig = require('./config/webpack.config');
// webpack setup
var compiler = webpack(webpackConfig);
app.use(webpackDevMiddleware(compiler, {
noInfo: true, publicPath: webpackConfig.output.publicPath
}));
-
Tutorial で記述した
example.js
をentry
に設定します。
config/webpack.config.js
:
module.exports = {
entry: [
'./public/javascripts/example.js'
],
output: {
path: __dirname + '/static/',
filename: 'bundle.js',
publicPath: '/static/'
},
module: {
loaders: [
{
test: /\.js$/,
loaders: ['babel'],
exclude: /node_modules/
}
]
},
resolve: {
extensions: ['', '.js']
}
};
-
marked
以外は Webpack で生成するので、layout.jade
から除外します。
views/layout.jade
:
doctype html
html
head
meta(charset='utf-8')
title React Tutorial
link(rel='stylesheet', href='/stylesheets/style.css')
script(src='https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js')
body
block content
-
Webpack が生成する
bundle.js
をscript
で参照するように編集します。
views/index.jade
:
extends layout
block content
#content
script(src='static/bundle.js')
-
JQuery や React を
import
に記述します。
public/javascripts/example.js
:
import $ from 'jquery';
import React from 'react';
import ReactDOM from 'react-dom';
次の Redux の Tutorial は ECMAScript 6 で記述しています。
Redux
Redux は Flux の Framework で主に Action, Reducer, Store で構成されます。
Yeoman
Redux の examples を参照するとディレクトリの構造にルールがあるのが解ります。
この Redux Tutorial は Yeoman の Redux Generator で出力された構造を参考にしています。
最初から React Tutorial を Redux で順に進めて行くので yo redux
コマンドの実行は不要です!
$ npm install --global yo
$ npm install --global generator-redux
$ yo redux
- actions
- Store にアプリケーションのデータを送信する情報が設置される。
- components
- Component(構成部品) が設置される。
- constants
-
Action と Reducer の
const
が設置される。
-
Action と Reducer の
- containers
-
App.js
にアプリケーションの起点が設置される。
-
- reducers
- Action に応じてアプリケーションの状態を変更する機構が設置される。
- store
- アプリケーションの状態を保持する機構と状態を更新する機構が設置される。
- utils
- DevTools などが設置される。
Tutorial
Tutorial 1
-
app
ディレクトリに各役割のディレクトリを設置しました。
app/index.js
:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './containers/App';
ReactDOM.render(
<App />,
document.getElementById('content')
);
- アプリケーションの起点の
App.js
を設置します。
app/containers/App.js
:
import React from 'react';
import Home from '../components/Home';
export default React.createClass({
render() {
return (
<div>
<Home />
</div>
);
}
});
- コンポーネントの起点の
Home.js
を設置します。
app/components/Home.js
:
import React, {Component} from 'react';
import CommentBox from './CommentBox';
class Home extends Component {
render() {
return (
<div>
<CommentBox />
</div>
);
}
}
export default Home
- コンポーネントに
CommentBox.js
を設置します。
app/components/CommentBox.js
:
import React, {Component} from 'react';
class CommentBox extends Component {
render() {
return (
<div className="commentBox">
Hello, world! I am a CommentBox.
</div>
);
}
}
export default CommentBox;
-
Webpack の
entry
をindex.js
に変更します。
config/webpack.config.js
:
module.exports = {
entry: [
'./app/index.js'
],
再び Hello, world! が来ました。
まだ、Redux は利用していません。
Tutorial 2,3
- コンポーネントはそれぞれのファイルに設置します。
app/components/CommentBox.js
:
import React, {Component} from 'react';
import CommentList from './CommentList';
import CommentForm from './CommentForm';
class CommentBox extends Component {
render() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList />
<CommentForm />
</div>
);
}
}
export default CommentBox;
- コンポーネントのファイルが Fat になるのを抑止されます。
app/components/CommentList.js
:
import React, {Component} from 'react';
class CommentList extends Component {
render() {
return (
<div className="commentList">
Hello, world! I am a CommentList.
</div>
);
}
}
export default CommentList;
- モジュールが個々にできるので、テストも小さくできます。
app/components/CommentForm.js
:
import React, {Component} from 'react';
class CommentForm extends Component {
render() {
return (
<div className="commentForm">
Hello, world! I am a CommentForm.
</div>
);
}
}
export default CommentForm;
ふたつの Hello, world! が表示されていますね。
しかしまだ、Redux は利用していません。
Tutorial 4,5
- 上位の CommentList から Comment にデータを流します。
app/components/CommentList.js
:
import React, {Component} from 'react';
import Comment from './Comment';
class CommentList extends Component {
render() {
return (
<div className="commentList">
<Comment author="Pete Hunt">This is one comment</Comment>
<Comment author="Jordan Walke">This is *another* comment</Comment>
</div>
);
}
}
export default CommentList;
- 下位の
props
でデータを表示しています。
app/components/Comment.js
:
import React, {Component} from 'react';
class Comment extends Component {
render() {
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
{this.props.children}
</div>
);
}
}
export default Comment;
ここでもまだ、Redux は利用していません。
Tutorial 6,7
-
ECMAScript 6 なので
function
の記述が不要です。
app/components/Comment.js
:
import React, {Component} from 'react';
class Comment extends Component {
rawMarkup() {
let rawMarkup = marked(this.props.children.toString(), {sanitize: true});
return { __html: rawMarkup };
}
render() {
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
<span dangerouslySetInnerHTML={this.rawMarkup()} />
</div>
);
}
}
export default Comment;
もちろん Redux は利用していません。
Tutorial 8,9,10
- データを Home から下位に流します。
app/components/Home.js
:
import React, {Component} from 'react';
import CommentBox from './CommentBox';
const data = [
{author: "Pete Hunt", text: "This is one comment"},
{author: "Jordan Walke", text: "This is *another* comment"}
];
class Home extends Component {
render() {
return (
<div>
<CommentBox data={data} />
</div>
);
}
}
export default Home
- データを CommentBox から下位に流します。
app/components/CommentBox.js
:
import React, {Component} from 'react';
import CommentList from './CommentList';
import CommentForm from './CommentForm';
class CommentBox extends Component {
render() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.props.data} />
<CommentForm />
</div>
);
}
}
export default CommentBox;
- データを CommentList から下位に流します。
app/components/CommentList.js
:
import React, {Component} from 'react';
import Comment from './Comment';
class CommentList extends Component {
render() {
const commentNodes = this.props.data.map((comment, index) => {
return (
<Comment author={comment.author} key={index}>
{comment.text}
</Comment>
);
});
return (
<div className="commentList">
{commentNodes}
</div>
);
}
}
export default CommentList;
Redux を利用しなくともディレクトリの構造やモジュール化のテクニックで
ファイルを小さく保つことができます。
Tutorial 11 - 14
GET /api/comments
から値が取得するアクションが発生するので、ここで Redux が登場します。
他に React と Redux をつなぐ React Redux や 非同期を調整する Redux Thunk のライブラリを利用します。
$ npm install redux --save
$ npm install react-redux --save
$ npm install redux-thunk --save
- まず、アクションを宣言します。
app/constants/ActionTypes.js
:
export const RECEIVE_COMMENTS = 'RECEIVE_COMMENTS';
- Comment の Search と Recieve のアクションを追加します。
- 非同期は ECMAScript 6 から利用できる Promise で宣言しています。
app/actions/CommentActions.js
:
import * as ActionTypes from '../constants/ActionTypes';
import $ from 'jquery';
export function recieveComments(comments) {
return {
type: ActionTypes.RECEIVE_COMMENTS,
comments
};
}
export function searchComments() {
return dispatch => {
let promise = new Promise((resolve, reject) => {
$.ajax({
url: '/api/comments',
dataType: 'json',
cache: false,
success(data) {
resolve(data);
},
error(xhr, status, err) {
reject(err);
}
});
});
promise.then((data) => {
dispatch(recieveComments(data));
}).catch((err) => {
console.error(err);
});
};
}
- 複数の Reducer は
index.js
で宣言します。
app/reducers/index.js
:
export {default as Comment} from './Comment';
- Comment の Recieve アクションから状態を取得します。
app/reducers/Comment.js
:
import * as ActionTypes from '../constants/ActionTypes';
const initialState = {comments: []};
export default function(state = initialState, action) {
switch (action.type) {
case ActionTypes.RECEIVE_COMMENTS:
return {comments: action.comments};
default:
return state;
}
}
- Store を生成するときに Middleware や Reducer を取り入れます。
app/store/configureStore.js
:
import {createStore, applyMiddleware, combineReducers} from 'redux';
import thunkMiddleware from 'redux-thunk';
import * as reducers from '../reducers/index';
let createStoreWithMiddleware = applyMiddleware(thunkMiddleware)(createStore);
const rootReducer = combineReducers(reducers);
export default function configureStore(initialState) {
return createStoreWithMiddleware(rootReducer, initialState);
}
- React Redux で Provider を宣言をして Store を流します。
app/containers/App.js
:
import React from 'react';
import {Provider} from 'react-redux';
import configureStore from '../store/configureStore';
import Home from '../components/Home';
const store = configureStore();
export default React.createClass({
render() {
return (
<div>
<Provider store={store}>
<Home />
</Provider>
</div>
);
}
});
-
Action を
dispatch
に登録する。
app/components/Home.js
:
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as CommentActions from '../actions/CommentActions';
import CommentBox from './CommentBox';
class Home extends Component {
render() {
const {dispatch, comments} = this.props;
const actions = bindActionCreators(CommentActions, dispatch);
return (
<div>
<CommentBox actions={actions} data={comments} />
</div>
);
}
}
export default connect(state => state.Comment)(Home)
- Search Comments のアクションを実施します。
app/components/CommentBox.js
:
import React, {Component} from 'react';
import CommentList from './CommentList';
import CommentForm from './CommentForm';
class CommentBox extends Component {
componentDidMount() {
this.props.actions.searchComments();
setInterval(this.props.actions.searchComments, 2000);
}
render() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.props.data} />
<CommentForm />
</div>
);
}
}
export default CommentBox;
構造的に小さなモジュールを繋げることは強いシステムになるので、Redux の考えは好ましいです。
Tutorial 15 - 20
- コメントを追加するアクションを宣言します。
app/constants/ActionTypes.js
:
export const ADD_COMMENT = 'ADD_COMMENT';
- Comment の Create と Add のアクションを追加します。
app/actions/CommentActions.js
:
export function addComment(comment) {
return {
type: ActionTypes.ADD_COMMENT,
comment
};
}
export function createComment(comment) {
return dispatch => {
dispatch(addComment(comment));
let promise = new Promise((resolve, reject) => {
$.ajax({
url: '/api/comments',
dataType: 'json',
type: 'POST',
data: comment,
success(data) {
resolve(data);
},
error(xhr, status, err) {
reject(err);
}
});
});
promise.then((data) => {
dispatch(recieveComments(data));
}).catch((err) => {
console.error(err);
});
};
}
- ADD_COMMENT でコメントが追加されます。
app/reducers/Comment.js
:
import * as ActionTypes from '../constants/ActionTypes';
const initialState = {comments: []};
export default function(state = initialState, action) {
switch (action.type) {
case ActionTypes.RECEIVE_COMMENTS:
return {comments: action.comments};
case ActionTypes.ADD_COMMENT:
return {comments: state.comments.concat([action.comment])};
default:
return state;
}
}
-
onCommentSubmit
にcreateComment
を設定します。
app/components/CommentBox.js
:
import React, {Component} from 'react';
import CommentList from './CommentList';
import CommentForm from './CommentForm';
class CommentBox extends Component {
componentDidMount() {
this.props.actions.searchComments();
setInterval(this.props.actions.searchComments, 2000);
}
render() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.props.data} />
<CommentForm onCommentSubmit={this.props.actions.createComment} />
</div>
);
}
}
export default CommentBox;
-
ReactDOM.findDOMNode
でrefs
の DOM を操作します。
app/components/CommentForm.js
:
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
class CommentForm extends Component {
handleSubmit(e) {
e.preventDefault();
const author = ReactDOM.findDOMNode(this.refs.author).value.trim();
const text = ReactDOM.findDOMNode(this.refs.text).value.trim();
if (!text || !author) {
return;
}
this.props.onCommentSubmit({author: author, text: text});
ReactDOM.findDOMNode(this.refs.author).value = '';
ReactDOM.findDOMNode(this.refs.text).value = '';
return;
}
render() {
return (
<form className="commentForm" onSubmit={this.handleSubmit.bind(this)}>
<input type="text" placeholder="Your name" ref="author" />
<input type="text" placeholder="Say something..." ref="text" />
<input type="submit" value="Post" />
</form>
);
}
}
export default CommentForm;
これで Redux の Comments Box が完成しました。
Redux も Tutorial が必要かなと思って、React Tutorial を題材にしましたが、Redux はもっと多機能なので学習を深めたい。
Tips
WebSocket
サーバーとの通信に Ajax でなく WebSocket を利用してみましょう。
WebSocket は Socket.IO のライブラリを利用します。
$ npm install socket.io --save
$ npm install socket.io-client --save
Server
- Express の 実行ファイルの最後に socket.io の設定を追加します。
bin/www
:
var io = require('socket.io')(server);
require('../channels/channel.js')(io);
- サーバーに
search comments
とrecieve comments
とcreate comment
のチャンネルを追加します。 - また、
setInterval
をクライアントからサーバーに移設します。
channels/channel.js
:
var fs = require('fs');
var channel = function(io) {
io.on('connection', function(socket) {
socket.on('search comments', function(){
fs.readFile('db/comments.json', function(err, data) {
socket.emit('recieve comments', JSON.parse(data));
});
});
socket.on('create comment', function(comment){
fs.readFile('db/comments.json', function(err, data) {
var comments = JSON.parse(data);
comments.push(comment);
fs.writeFile('db/comments.json', JSON.stringify(comments, null, 4), function(err) {
socket.emit('recieve comments', comments);
});
});
});
setInterval(function(){
fs.readFile('db/comments.json', function(err, data) {
socket.emit('recieve comments', JSON.parse(data));
});
}, 2000);
})
};
module.exports = channel;
Client
-
$.ajax
(jQuery) をsocket.emit
に置換えます。
app/actions/CommentActions.js
:
import * as ActionTypes from '../constants/ActionTypes';
import io from 'socket.io-client';
export const socket = io('http://localhost:3000');
export function recieveComments(comments) {
return {
type: ActionTypes.RECEIVE_COMMENTS,
comments
};
}
export function addComment(comment) {
return {
type: ActionTypes.ADD_COMMENT,
comment
};
}
export function searchComments() {
return dispatch => {
socket.emit('search comments');
};
}
export function createComment(comment) {
return dispatch => {
socket.emit('create comment', comment);
};
}
-
componentWillMount
にrecieve comments
の Subscription を設置する。
app/components/CommentBox.js
:
import React, {Component} from 'react';
import CommentList from './CommentList';
import CommentForm from './CommentForm';
import io from 'socket.io-client';
export const socket = io('http://localhost:3000');
class CommentBox extends Component {
componentWillMount() {
const {actions} = this.props;
socket.on('recieve comments', function(comments) {
actions.recieveComments(comments);
});
}
componentDidMount() {
this.props.actions.searchComments();
}
render() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.props.data} />
<CommentForm onCommentSubmit={this.props.actions.createComment} />
</div>
);
}
}
export default CommentBox;
サーバーに socket.broadcast.emit
を追加すると setInterval
は不要です。
Comment Box は Socket.IO の Rooms 機能で少し手を入れるとチャットルームが作れますね。
PropTypes
PropTypesは Component の引数(props
) を定義することができます。
可読性が向上するので利用しましょう。(型も豊富にあります。)
- PropTypes.array
- PropTypes.bool
- PropTypes.func
- PropTypes.number
- PropTypes.object
- PropTypes.string
- PropTypes.node
- PropTypes.element
- PropTypes.instanceOf(Message)
- PropTypes.oneOf(['News', 'Photos'])
- PropTypes.oneOfType([PropTypes.string, PropTypes.number)
- PropTypes.arrayOf(PropTypes.number)
- PropTypes.objectOf(PropTypes.number)
- PropTypes.shape({color: PropTypes.string, fontSize: PropTypes.number})
- PropTypes.any.isRequired
Example
-
{Component, PropTypes}
で宣言をして、propTypes = {}
で定義します。
app/components/Comment.js
:
import React, {Component, PropTypes} from 'react';
class Comment extends Component {
rawMarkup() {
let rawMarkup = marked(this.props.children.toString(), {sanitize: true});
return { __html: rawMarkup };
}
render() {
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
<span dangerouslySetInnerHTML={this.rawMarkup()} />
</div>
);
}
}
Comment.propTypes = {
author: PropTypes.string.isRequired,
children: PropTypes.string.isRequired
};
export default Comment;
Enjoy React