9
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

React Tutorial Example (Convention)

Last updated at Posted at 2016-06-21

ReactRedux には推奨されている規約がいくつかあります。
T.J. の Frontend BoilerplateAirbnb 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 に移動したので entryapp/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 Commentapp/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 CommentListapp/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 CommentFormapp/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 CommentBoxapp/components/CommentBox/index.js に移動します。
  • this.props.showCommentsthis.props.actions.showComments に変更します。
  • this.props.addCommentthis.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 のプロパティに dataactions を設定します。
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)
  • CommentBoxApp に変更します。
  • 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
  • createStoreapp/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
  • アクションに createActionshowComments を作成します。
  • アクションに createActionaddComment を作成します。
app/actions/index.js
import { createAction } from 'redux-actions'

export const showComments = createAction('show_comments')
export const addComment = createAction('add_comment')
  • リデューサに handleActionsreducerMap を設定します。
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)
  • コンポーネントに bindActionCreatorsActions を設定します。
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 を ローカルスコープ にしましょう!

9
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?