あいつの日誌β

働きながら旅しています。

React tutorial (7)

今回は redux-form について触れます。 おそらく React, Redux で管理画面作る場合はこの module を使うことになると思います。

github.com

ちなみに react-redux-form という似た名前のモジュールがありますが優劣に関してはここでは言及しません。 やりたいことができるならどっちを選んでも正解だと思います。

準備

npm install --save redux-form
touch src/containers/CreatePost.js

目次

  • 開発環境を準備
  • React の基本的な Life Cycle に触れる
  • redux に触れる
  • redux-saga に触れる
  • react router に触れる
  • npm で公開されている components を導入して echo system を体感する
  • redux-form に触れる <= 今日やること
  • react-select に触れる

各種ファイル作成

create src/containers/CreatePost.js

import React, {Component} from 'react'
import { connect } from 'react-redux'
import { Field, reduxForm } from 'redux-form'
import { createPost } from '../actions'

const renderField = ({ input, label, type, meta: { touched, error } }) => (
  <div className="form-group">
    <label htmlFor={label}>{label}</label>
    <input className="form-control" {...input} placeholder={label} type={type}/>
    <p className="text-danger">{touched && (error && <span>{error}</span>)}</p>
  </div>
)

const renderTextAreaField = ({ input, label, type, meta: { touched, error } }) => (
  <div className="form-group">
    <label htmlFor={label}>{label}</label>
    <textarea className="form-control" {...input} placeholder={label} type={type} rows="10"/>
    <p className="text-danger">{touched && (error && <span>{error}</span>)}</p>
  </div>
)

let PostForm = (props) => {
  const { handleSubmit, pristine, submitting } = props
  return (
    <form onSubmit={handleSubmit}>
      <Field name="title" type="text" label="Title" component={renderField} />
      <Field name="body" type="text" label="Body" component={renderTextAreaField} />
      <div style={{marginTop: "30px"}}>
        <button className="btn btn-primary" type="submit" disabled={pristine || submitting}>Submit</button>
      </div>
    </form>
  )
}

const validate = values => {
  const errors = {}

  if (!values.title) {
    errors.title = 'Required'
  } else if (values.title.length > 15) {
    errors.title = 'Must be 15 characters or less'
  }

  if (!values.body) {
    errors.body = 'Required'
  }

  return errors
}

PostForm = reduxForm({
    form: "post",
    validate,
    enableReinitialize: true,
})(PostForm)

class CreatePost extends Component {

  handleSubmit() {
    const params = this.props.form.post.values
    // Note: the resource will not be really created on the server but it will be faked as if.
    this.props.dispatch(createPost({params}))
  }

  render() {
    return (
      <PostForm onSubmit={this.handleSubmit.bind(this)} />
    )
  }
}

const select = state => (state)

export default connect(select)(CreatePost)

edit src/Root.js

import 'babel-polyfill'
import 'bootstrap'
import 'bootstrap/dist/css/bootstrap.css'
import React from 'react'
import { Provider } from 'react-redux'
import { Router, Route, IndexRoute, browserHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'

import configureStore from './store/configureStore'
import App from './containers/App'
import Home from './containers/Home'
import Posts from './containers/Posts'
import CreatePost from './containers/CreatePost'

const store = configureStore()
const history = syncHistoryWithStore(browserHistory, store)

const NotFound = () => (<div><span>NOT FOUND</span></div>)

export default (props) => (
  <Provider store={store}>
    <Router history={history}>
      <Route path="/" component={App}>
        <IndexRoute component={Home} />
        <Route path="posts">
          <IndexRoute component={Posts} />
          <Route path="create" component={CreatePost} />
        </Route>
      </Route>
      <Route path="*" component={NotFound}/>
    </Router>
  </Provider>
)

edit src/actions.js

import { createAction } from "redux-actions"
import api from './api'

export const FETCH_REQUESTED = "FETCH_REQUESTED"
export const FETCH_SUCCESSED = "FETCH_SUCCESSED"
export const FETCH_FAILED    = "FETCH_FAILED"

export const fetchRequested = createAction(FETCH_REQUESTED)
export const fetchSuccessed = createAction(FETCH_SUCCESSED)
export const fetchFailed    = createAction(FETCH_FAILED)

export function getPosts({userId}) {
  return fetchRequested(() => {
    return api.getPosts({userId})
  })
}

export function createPost({params}) {
  return fetchRequested(() => {
    return api.createPost({params})
  })
}

edit src/api.js

import axios from 'axios'

const BASE_URL = window.location.origin

export function getPosts({userId}) {
  return axios.get(`${BASE_URL}/api/posts?userId=${userId}`).then((res) => {
    return { "posts": res.data }
  })
}

export function createPost({params}) {
  return axios.post(`${BASE_URL}/api/posts`, params).then((res) => {
    return { "post": res.data }
  })
}

export default { getPosts, createPost }

edit src/containers/App.js

import React, { Component } from 'react'
import { Link } from 'react-router'
import { connect } from 'react-redux'
import Notifications from 'reapop'
import theme from 'reapop-theme-wybo'

class App extends Component {

  render() {
    return (
      <div className="container">
        <Notifications theme={theme}/>
        <h1>Single Page Application</h1>
        <p><Link to="/">Home</Link> | <Link to="/posts">Posts</Link> | <Link to="/posts/create">Create Post</Link></p>
        <div style={{ marginTop: '1.5em' }}>{this.props.children}</div>
      </div>
    )
  }
}

export default connect(state => (state))(App)

edit src/reducers/index.js

import { combineReducers } from 'redux'
import { routerReducer } from 'react-router-redux'
import { reducer as formReducer } from 'redux-form'
import { reducer as notificationsReducer } from 'reapop'

import { FETCH_SUCCESSED } from '../actions'

const fetchInitialState = {
  posts: []
}

function fetch(state = fetchInitialState, action) {
  switch (action.type) {
  case FETCH_SUCCESSED:
    return Object.assign({}, state, action.payload)
  default:
    return state
  }
}

const rootReducer = combineReducers({
  notifications: notificationsReducer(),
  fetch,
  routing: routerReducer,
  form: formReducer,
})

export default rootReducer

説明

reduxForm 関数でコンポーネントを wrap していますがこれによって this.props.form.post にフォームの状態が保持されるようになります。 この wrap された関数内でしか Field コンポーネントが動かないのでご注意ください。

公式サイトには example が多数掲載されているのでそちらも合わせてご確認ください。

まとめ

今回は redux-form の使い方に触れました。 次回は react-select という cool なコンポーネントと redux-form を共存させる方法について紹介します。