React や Redux には推奨されている規約がいくつかあります。
T.J. の Frontend Boilerplate や Airbnb React Style Guide を参考にして
前回 のアプリケーションに規約を適用してみましょう。
Learning
- ファイル、ディレクトリの 命名規約 を適用する。
- アクションに Flux Standard Action を利用する。
Convention
React の Data Flow は Container -> Component の単一方向です。
Redux の Data Flow は Action -> Reducer -> Store の単一方向です。
つまり Data Flow の Context でディレクトリ, ファイルに分離します。
- ディレクトリには
index.js
が設置されimport
ではディレクトリの名前を指定する。 - コンテナ や コンポーネント はキャメルでディレクトリの名前を指定する。
- コンテナ や コンポーネント は1つの ファイル で1つの クラス を定義する。
- アクションは Flux Standard Action で定義する。
- ディレクトリの名前で設置する種類を定義する。
-
containers
: コンポーネント(部品)を載せる コンテナ を設置する。 -
components
: アプリケーションのコンポーネント(部品)を設置する。 -
actions
: アプリケーションのアクション(行動)を設置する。 -
reducers
: アクション(行動)による状態を変更する機能を設置する。 -
store
: アプリケーションの状態を保持する機構を設置する。
-
Environment
- node: v4.4.5
- npm: v3.9.6
Comment Box Form
- 完成される Source Code のファイルリストです。
$ tree -a -I node_modules
.
├── .babelrc
├── app
│ ├── index.js
│ ├── actions
│ │ └── index.js
│ ├── components
│ │ ├── Comment
│ │ │ └── index.js
│ │ ├── CommentBox
│ │ │ └── index.js
│ │ ├── CommentForm
│ │ │ └── index.js
│ │ └── CommentList
│ │ └── index.js
│ ├── containers
│ │ └── App
│ │ └── index.js
│ ├── reducers
│ │ └── index.js
│ └── store
│ └── index.js
├── index.html
├── package.json
├── style.css
└── webpack.config.js
Let's hands-on
Setup application
-
git clone
コマンドでアプリケーションをダウンロードします。 -
npm install
コマンドで依存するモジュールをインストールします。
$ git clone https://github.com/ogom/react-comment-box-example.git
$ cd react-comment-box-example
$ git checkout es2015
$ npm install
Start HTTP Server
-
npm start
コマンドで Webアプリケーション を実行します。 - ブラウザで http://localhost:4000 をロードすると Comment Box Example が表示されます。
$ npm start
$ open http://localhost:4000
(API Server は React Tutorial Example (Express) をご利用ください。)
Webpack Entry
- まずは
app
ディレクトリを作成します。 - そこに
index.js
ファイルを移動します。
$ mkdir app/
$ mv index.js app/.
-
app/index.js
に移動したのでentry
をapp/index
に変更します。
webpack.config.js
var webpack = require('webpack')
module.exports = {
- entry: './index',
+ entry: './app/index',
output: {
filename: 'bundle.js'
},
-
app.js
は一つ上の階層なので../app
に変更します。
app/index.js
-import CommentBox from './app'
+import CommentBox from '../app'
Containers
- つぎにコンテナの
app/containers
ディレクトリを作成します。 - そこに
app.js
ファイルを移動します。
$ mkdir app/containers
$ mkdir app/containers/App
$ mv app.js app/containers/App/index.js
-
../app
を./containers/App
に変更します。
app/index.js
-import CommentBox from '../app'
+import CommentBox from './containers/App'
Components
- そしてコンポーネントの
app/components
ディレクトリを作成します。
$ mkdir app/components
Comment
-
app/components/Comment/index.js
ファイルを作成します。
$ mkdir app/components/Comment
$ touch app/components/Comment/index.js
- コンテナの
class Comment
をapp/components/Comment/index.js
に移動します。- コードが差分で表示されていない場合は、全て差し替えてください。
app/components/Comment/index.js
import React, { Component } from 'react'
import Remarkable from 'remarkable'
class Comment extends Component {
rawMarkup() {
const md = new Remarkable()
const rawMarkup = md.render(this.props.children.toString())
return { __html: rawMarkup }
}
render() {
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
<span dangerouslySetInnerHTML={this.rawMarkup()} />
</div>
)
}
}
export default Comment
- コンテナの
Remarkable
はコンポーネントに移動したので削除します。 - コンテナに
Comment
コンポーネントをインポートします。
app/containers/App/index.js
-import Remarkable from 'remarkable'
+import Comment from '../../components/Comment'
CommentList
-
app/components/CommentList/index.js
ファイルを作成します。
$ mkdir app/components/CommentList
$ touch app/components/CommentList/index.js
- コンテナの
class CommentList
をapp/components/CommentList/index.js
に移動します。
app/components/CommentList/index.js
import React, { Component } from 'react'
import Comment from '../../components/Comment'
class CommentList extends Component {
render() {
const commentNodes = this.props.data.map((comment) => {
return (
<Comment author={comment.author} key={comment.id}>
{comment.text}
</Comment>
)
})
return (
<div className="commentList">
{commentNodes}
</div>
)
}
}
export default CommentList
- コンテナの
Comment
はコンポーネントに移動したので削除します。 - コンテナに
CommentList
コンポーネントをインポートします。
app/containers/App/index.js
-import Comment from '../../components/Comment'
+import CommentList from '../../components/CommentList'
CommentForm
-
app/components/CommentForm/index.js
ファイルを作成します。
$ mkdir app/components/CommentForm
$ touch app/components/CommentForm/index.js
- コンテナの
class CommentForm
をapp/components/CommentForm/index.js
に移動します。
app/components/CommentForm/index.js
import React, { Component } from 'react'
class CommentForm extends Component {
constructor(props) {
super(props)
this.state = {author: '', text: ''}
}
handleAuthorChange(e) {
this.setState({author: e.target.value})
}
handleTextChange(e) {
this.setState({text: e.target.value})
}
handleSubmit(e) {
e.preventDefault()
const author = this.state.author.trim()
const text = this.state.text.trim()
if (!text || !author) {
return
}
this.props.onCommentSubmit({author: author, text: text})
this.setState({author: '', text: ''})
}
render() {
return (
<form className="commentForm" onSubmit={this.handleSubmit.bind(this)}>
<input
type="text"
placeholder="Your name"
value={this.state.author}
onChange={this.handleAuthorChange.bind(this)}
/>
<input
type="text"
placeholder="Say something..."
value={this.state.text}
onChange={this.handleTextChange.bind(this)}
/>
<input type="submit" value="Post" />
</form>
)
}
}
export default CommentForm
- コンテナに
CommentForm
コンポーネントをインポートします。
app/containers/App/index.js
+import CommentForm from '../../components/CommentForm'
CommentBox
-
app/components/CommentBox/index.js
ファイルを作成します。
$ mkdir app/components/CommentBox
$ touch app/components/CommentBox/index.js
- コンテナの
class CommentBox
をapp/components/CommentBox/index.js
に移動します。 -
this.props.showComments
をthis.props.actions.showComments
に変更します。 -
this.props.addComment
をthis.props.actions.addComment
に変更します。
(Middleware は利用しないでコンテナに Ajax を残しています。)
app/components/CommentBox/index.js
import React, { Component } from 'react'
import CommentList from '../../components/CommentList'
import CommentForm from '../../components/CommentForm'
import $ from 'jquery'
class CommentBox extends Component {
constructor(props) {
super(props)
this.state = {data: []}
}
loadCommentsFromServer() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.props.actions.showComments(data)
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString())
}.bind(this)
})
}
handleCommentSubmit(comment) {
const comments = this.state.data
// Optimistically set an id on the new comment. It will be replaced by an
// id generated by the server. In a production application you would likely
// not use Date.now() for this and would have a more robust system in place.
comment.id = Date.now()
this.props.actions.addComment(comment)
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'POST',
data: comment,
success: function(data) {
this.props.actions.showComments(data)
}.bind(this),
error: function(xhr, status, err) {
this.props.actions.showComments(comments)
console.error(this.props.url, status, err.toString())
}.bind(this)
})
}
componentDidMount() {
this.loadCommentsFromServer()
setInterval(this.loadCommentsFromServer.bind(this), this.props.pollInterval)
}
render() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.props.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit.bind(this)} />
</div>
)
}
}
export default CommentBox
- コンテナに
CommentBox
コンポーネントをインポートします。 -
CommentBox
のプロパティにdata
とactions
を設定します。
app/containers/index.js
import React, { Component } from 'react'
import CommentBox from '../../components/CommentBox'
import { connect } from 'react-redux'
class App extends Component {
render() {
const { data, actions } = this.props
return (
<div>
<CommentBox
data={data}
actions={actions}
url="http://localhost:3000/api/comments"
pollInterval={2000}
/>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
data: state
}
}
const mapDispatchToProps = (dispatch) => {
return {
actions: {
showComments(comments) {
dispatch({type: 'show_comments', comments: comments})
},
addComment(comment) {
dispatch({type: 'add_comment', comment: comment})
}
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App)
-
CommentBox
をApp
に変更します。 -
App
のプロパティを<App />
に変更します。
app/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './containers/App'
const store = createStore((state=[], action) => {
switch (action.type) {
case 'show_comments':
return action.comments
case 'add_comment':
return state.concat([action.comment])
default:
return state
}
})
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('content')
)
Store
-
app/store/index.js
ファイルを作成します。
$ mkdir app/store
$ touch app/store/index.js
-
createStore
をapp/store/index.js
に移動します。
app/store/index.js
import { createStore } from 'redux'
export default () => {
const store = createStore((state=[], action) => {
switch (action.type) {
case 'show_comments':
return action.comments
case 'add_comment':
return state.concat([action.comment])
default:
return state
}
})
return store
}
-
configureStore
をインポートします。
app/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import App from './containers/App'
import configureStore from './store'
const store = configureStore()
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('content')
)
Reducers
-
app/reducers/index.js
ファイルを作成します。
$ mkdir app/reducers
$ touch app/reducers/index.js
- リデューサを
app/reducers/index.js
に移動します。
app/reducers/index.js
export default (state=[], action) => {
switch (action.type) {
case 'show_comments':
return action.comments
case 'add_comment':
return state.concat([action.comment])
default:
return state
}
}
- ストアに
reducers
をインポートします。
app/store/index.js
import { createStore } from 'redux'
import reducers from '../reducers'
export default () => {
return createStore(reducers)
}
Actions
- Flux Standard Action のモジュールをインストールします。
$ npm install redux-actions --save-dev
-
app/reducers/index.js
ファイルを作成します。
$ mkdir app/actions
$ touch app/actions/index.js
- アクションに
createAction
でshowComments
を作成します。 - アクションに
createAction
でaddComment
を作成します。
app/actions/index.js
import { createAction } from 'redux-actions'
export const showComments = createAction('show_comments')
export const addComment = createAction('add_comment')
- リデューサに
handleActions
でreducerMap
を設定します。
app/reducers/index.js
import { handleActions } from 'redux-actions'
const initialState = []
const reducerMap = {
show_comments(state, action) {
return action.payload
},
add_comment(state, action) {
return state.concat([action.payload])
}
}
export default handleActions(reducerMap, initialState)
- コンポーネントに
bindActionCreators
でActions
を設定します。
app/containers/App/index.js
import React, { Component } from 'react'
import CommentBox from '../../components/CommentBox'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as Actions from '../../actions'
class App extends Component {
render() {
const { data, actions } = this.props
return (
<div>
<CommentBox
data={data}
actions={actions}
url="http://localhost:3000/api/comments"
pollInterval={2000}
/>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
data: state
}
}
const mapDispatchToProps = (dispatch) => {
return {
actions: bindActionCreators(Actions, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App)
Congrats!
コード を Component, Container, Action, Reducer, Store の ファイル に分離できました。
次は CSS を ローカルスコープ にしましょう!