Today I’m going to share my experience with writing React components that collect user input, handle image uploading, and communicate with API to load or persist data.
For the purpose of persisting the state of input fields you could write your own components, but there’s absolutely no reason to do so, as there are several npm packages exist that do it all for you, and have a decent documentation & support available. I’ll list just a few options: redux-forms, react-redux-form, formsy-react.
My package of choice is react-redux-form. Its documentation turned out to be easily understood, and package itself has all features you may need to use with your forms. Though redux-forms has more stars on github and it seems to be a good choice as well, I wish it had a better documentation, and code samples for main use cases.
Alright, so let’s start off by creating a form reducer for Redux store:
1 2 3 4 5 6 7 8 |
import { combineReducers } from 'redux' import { combineForms } from 'react-redux-form' export default combineReducers({ forms: combineForms({ myForm: {}, }, 'forms', { key: 'f' }) }) |
Here we use combineForms() function that allows us to have multiple forms. If you don’t need more than one form, however, you may consider using formReducer() instead.
Then we’re getting back to our component and create a form:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
import React, { PropTypes } from 'react' import { connect } from 'react-redux' import { Control, Form, Errors } from 'react-redux-form' import { isEmpty } from 'validator' export class MyForm extends React.Component { static propTypes = { myForm: PropType.object.isRequired, submit: PropType.func.isRequired } handleSubmit(user) { this.props.submit(user) } render() { let { myForm } = this.props return ( <Form model="forms.myForm" onSubmit={(formModel) => this.handleSubmit(formModel)}> <label>First name</label> <Control.text model=".firstName" placeholder="Enter first name" validators={{ required: (val) => val && !isEmpty(val) }} /> <Errors model=".firstName" show="touched" messages={{ required: "This field is required" }} /> <label>Last name</label> <Control.text model=".lastName" placeholder="Enter last name" validators={{ required: (val) => val && !isEmpty(val) }} /> <Errors model=".lastName" show="touched" messages={{ required: "This field is required" }} /> <label>Gender</label> <Control.select model=".gender" validators={{ required: (val) => val && !isEmpty(val) }}> <option value="0">male</option> <option value="1">female</option> </Control.select> <Errors model=".gender" show="touched" messages={{ required: "This field is required" }} /> <label>Avatar</label> <Control.file model=".avatar" validators={{}} /> <Errors model=".avatar" show="touched" messages={{}} /> <button type="submit" disabled={myForm.$form.pending}> { myForm.$form.pending ? 'Loading' : 'Sign Up' } </button> </Form>) } } |
Here we’ve created a simple form with name, gender, and avatar fields. It also validates required inputs and shows validation messages below.
The component accepts two props: myForm which is an object that holds our form’s state (I’ll explain what that means below), and submit – a function that handles form submission.
Next, we’ll create a container that will pass required props to the component:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { connect } from 'react-redux' import MyFrom from './MyForm' import { userSignup } from './actions' const mapStateToProps = (state) => ({ myForm: state.forms.f.myForm }) const mapDispatchToProps = (dispatch) => ({ submit: (user) => dispatch(userSignup(user)) }) export default connect( mapStateToProps, mapDispatchToProps )(MyForm) |
Here, myForm refers to the state of our form, which includes all low level details such as validity of each field, general state of the form (pending/submitted), and so on. Check out myForm object to see all contained data.
The last step is creating an action that makes API call, sets validation errors from the backend, if there’s any, or marks form as submitted otherwise.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import request from 'superagent' import { actions as formActions } from 'react-redux-form' export const userSignup = (user) => { const signupRequest = request.post('/api/v1/user/', user) .then(response => true, (err, res) => { if (err.response && err.response.body) { return Promise.reject(err.response.body) } }) return formActions.submitFields('forms.myForm', signupRequest) } |
formActions.submitFields dispatches an action which will update the form status, and set validity. Note, that in order to have validation messages set correctly, in case of failure, the response from /api/v1/user should be in the following format:
1 2 3 4 |
{ "firstName": ["First name can be 50 characters long maximum", "First letter has to be capital"], "avatar": ["Maximum allowed image size is 1Mb"] } |
Where each key in the object corresponds with a key from the form model.
Now we know how to build simple, yet customizable forms that validate user input, store fields data in redux store, can transmit data to a backend, and handle backend responses to display additional error messages.