今回は redux-form について触れます。 おそらく React, Redux で管理画面作る場合はこの module を使うことになると思います。
ちなみに 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 を共存させる方法について紹介します。