今回は 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
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 を共存させる方法について紹介します。