Log In

React Forms


by Yariv Katz

Web forms are the main way to grab data from our user in a web application. Due to it's importance we have to feel comfortable to deal with user input, and perform validation on the user input. React is a UI library so it brings us basic tools to deal with forms, but if our application is getting more complex we will want better tools to deal with forms in our application. In this lesson we will learn how to deal with forms in react and about the popular libraries we can install to help us with forms. We will create the same form using different tools, the form we will create will be a registration form. Our registration form will contain the following fields and validations:
- username - Text field to enter username which needs to contain alphanumeric characters, the field will be required with max length of 20 characters.
- Email - email field, the user will have to enter a valid email, this field will be required as well and up to 100 characters.
- Full name - the user will enter his first and last name, this field is required and we will validate that there are two words seperated with a space and up to 100 characters.
- Password and repeat password - those two fields will have to match, the password and repeat have to be minimum of 5 characters.
The form we will create will display validation error messages, the validation messages will appear after the field is dirty (some text was entered even if the user decided to delete the text he entered) and the user blured from that field. We will use bootstrap for the form appearance.

Bootstrap react app

let's start a new react app using the cli create-react-app. We will also install bootstrap. In the terminal type:

> npx create-react-app react-forms-tutorial
> cd react-forms-tutorial
> npm install bootstrap --save

We need to include bootstrap in our app global style file. Modify the file index.css and add the following at the top of the file:

@import "~bootstrap/dist/css/bootstrap.css";

We are now ready to start experimenting with the tools we have to deal with forms.

Controlled form

React out of the box is giving us certain basic tools to deal with forms, those tools might be sufficient for simple forms but as our app grows more complex it will be hard to manage with simply using those tools. Still we have to familiarize ourselves with those tools before moving to the most advanced libraries. The first tool we need to learn is handling forms in a controlled manner, this means that the form elements will be controlled from the state of the component. Changing the input will first change the state and the control value will be set according to the state. Let's create our form using the controlled way. Create a new file in your src folder called ControlledRegister.js with the following code:

import React, {Component} from 'react';

export default class ControlledRegister extends Component {
    messages = {
        required: 'This field is required',
        email: 'Invalid mail',
        fullName: 'Please specify first and last name',
        minLength: 'Too short',
        repeat: 'Must match the password field'
    }

    state = {
        username: '',
        usernameDirty: false,
        usernameErrors: ['required'],
        email: '',
        emailDirty: false,
        emailErrors: ['required'],
        fullName: '',
        fullNameDirty: false,
        fullNameErrors: ['required'],
        password: '',
        passwordDirty: false,
        passwordErrors: ['required'],
        repeat: '',
        repeatDirty: false,
        repeatErrors: ['required']
    }

    controlBlur = (event) => {
        this.setState({
            [event.target.name + 'Dirty']: true
        })
    }

    handleInputChange = (event) => {
        const target = event.target;
        const name = target.name;
        const value = target.value;

        // populate errors
        const errors = [];
        if (target.pattern.length > 0) {
            const reg = new RegExp(target.pattern)
            if (!reg.test(value)) {
                name === 'email' ? errors.push('email') : errors.push('pattern')
            }
        }
        if (name === 'fullName') {
            const splittedArray = value.split(' ');
            if (splittedArray.length !== 2) {
                errors.push('fullName')
            }
        }
        if (target.required && value === '') errors.push('required');
        if (target.minLength > 0 && value.length < target.minLength && value.length > 0) errors.push('minLength');
        if (name === 'repeat' && value !== this.state.password) {
            errors.push('repeat');
        }
        
        this.setState({
            [name]: value,
            [name + 'Errors']: errors
        })
    }

    checkErrors = () => {
        return (this.state.usernameErrors.length > 0 
            || this.state.emailErrors.length > 0
            || this.state.fullNameErrors.length > 0
            || this.state.passwordErrors.length > 0
            || this.state.repeatErrors.length > 0
        )
    }

    renderErrors = (field) => {
        if (this.state[field + 'Errors'].length === 0 || !this.state[field + 'Dirty']) return null;
        const errors = [];
        for (let error of this.state[field + 'Errors']) {
            errors.push(<li>{this.messages[error]}</li>)
        }
        return <div className="alert alert-danger"><ul>{errors}</ul></div>
    }

    submit = () => {
        alert('success');
    }

    render() {
        return (
            <form noValidate>
                <h2>Controlled form</h2>
                <div className="form-group">
                    <label>Username</label>
                    <input 
                        value={this.state.username}
                        onBlur={this.controlBlur}
                        onChange={this.handleInputChange}
                        type="text" 
                        name="username" 
                        required
                        className="form-control" 
                        maxLength="20" />
                    {this.renderErrors('username')}
                </div>
                <div className="form-group">
                    <label>Email</label>
                    <input 
                        value={this.state.email}
                        onChange={this.handleInputChange}
                        onBlur={this.controlBlur}
                        type="email" 
                        name="email" 
                        required
                        pattern="[a-zA-Z0-9.!#$%&amp;’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+"
                        className="form-control" 
                        maxLength="100" />
                    {this.renderErrors('email')}
                </div>
                <div className="form-group">
                    <label>Full Name</label>
                    <input 
                        value={this.state.fullName}
                        onChange={this.handleInputChange}
                        onBlur={this.controlBlur}
                        type="text" 
                        name="fullName" 
                        required
                        className="form-control" 
                        maxLength="100" />
                    {this.renderErrors('fullName')}
                </div>
                <div className="form-group">
                    <label>Password</label>
                    <input 
                        value={this.state.password}
                        onChange={this.handleInputChange}
                        onBlur={this.controlBlur}
                        type="password" 
                        name="password" 
                        required
                        className="form-control" 
                        minLength="5" />
                    {this.renderErrors('password')}
                </div>
                <div className="form-group">
                    <label>Repeat password</label>
                    <input 
                        value={this.state.repeat}
                        onChange={this.handleInputChange}
                        onBlur={this.controlBlur}
                        type="password" 
                        name="repeat" 
                        required
                        className="form-control" 
                        minLength="5" />
                    {this.renderErrors('repeat')}
                </div>
                <div className="form-group">
                    <button 
                        type="submit" 
                        disabled={this.checkErrors()}
                        className="btn btn-primary">Submit</button>
                </div>
            </form>
        )
    }
}

A lot of work to be done in a single component with a form containing validation and error messages. Let's go over the main things in this code. For each input we are saving the dirty state of that control, an array of strings containing the errors, and the value. When an input is blured we set the dirty to true and display the errors if exists. When the input is changed we set the value and check for errors. The submit button will be disabled until there are not errors.

Formik

We will try to do a similar form like the above using Formik and Yup. Formik is a lightweight library that helps us deal with Forms and validation. Yup is a lightweight library that helps you deal with validation. Let's start by installing the libraries. In your terminal type:

> npm install formik --save
> npm install yup --save

We will create the same form as above in a seperate component using Formik, we will also use Yup for the validation. Create a new file called FormikRegister.js with the following code:

import React, {Component} from 'react';
import {Formik, Form, Field, ErrorMessage} from 'formik';
import * as Yup from 'yup';

const RegisterSchema = Yup.object().shape({
    username: Yup.string()
        .max(20, 'Too long')
        .required('This field is required'),
    email: Yup.string()
        .email('Invalid mail')
        .required('This field is required'),
    fullName: Yup.string()
        .required('This field is required')
        .test('num-words', 'You must supply 2 words', function(value) {
            if (value && value.split(' ').length === 2) {
                return true;
            }
            return false;
        }),
    password: Yup.string()
        .required('This field is required')
        .min(5, 'Too short!'),
    repeat: Yup.string()
        .required('This field is required')
        .min(5, 'Too short!')
        .oneOf([Yup.ref('password'), null])
})

export default class FormikRegister extends Component {
    submit = () => {
        alert('success');
    }

    render() {
        return (
            <Formik
                initialValues={ {
                    username: '',
                    email: '',
                    fullName: '',
                    password: '',
                    repeat: ''
                } }
                validationSchema={RegisterSchema}
                onSubmit={this.submit}
                >
                {
                    ({isSubmiting, isValid}) => (
                        <Form>
                            <h2>Formik form</h2>
                            <div className="form-group">
                                <label>Username</label>
                                <Field type="text" name="username" className="form-control" />
                                <ErrorMessage name="username" component="div" className="alert alert-danger" />
                            </div>
                            <div className="form-group">
                                <label>Email</label>
                                <Field type="email" name="email" className="form-control" />
                                <ErrorMessage name="email" component="div" className="alert alert-danger" />
                            </div>
                            <div className="form-group">
                                <label>Full Name</label>
                                <Field type="text" name="fullName" className="form-control" />
                                <ErrorMessage name="fullName" component="div" className="alert alert-danger" />
                            </div>
                            <div className="form-group">
                                <label>Password</label>
                                <Field type="password" name="password" className="form-control" />
                                <ErrorMessage name="password" component="div" className="alert alert-danger" />
                            </div>
                            <div className="form-group">
                                <label>Repeat Password</label>
                                <Field type="password" name="repeat" className="form-control" />
                                <ErrorMessage name="repeat" component="div" className="alert alert-danger" />
                            </div>
                            <div className="form-group">
                                <button 
                                    className="btn btn-primary"
                                    type="submit" disabled={isSubmiting || !isValid}>Submit</button>
                            </div>
                        </Form>
                    )
                }
            </Formik>
        );
    }
}

Our code became more simple than the last one. Let's go over the code we wrote. Formik plays perfectly with a validation package called Yup. The way it works we create a yup schema of our fields and validations using yup. Yup will contain must of the validations we need out of the box and we can alway create a custom validation like we did in the fullName field. We attach the schema to our Formik component on the prop validationSchema. We are creating our form using components from Formik where the Field component will map to a certain input. ErrorMessage will display an error message based on the name if an error exists.

Redux forms

The last package to examine is redux-forms which helps us manage our form state using redux. To use this package we will first need to connect our app to redux. Let's install the redux package and the react-redux package. We will also need to install the redux-forms package In your terminal type:

> npm install redux --save
> npm install react-redux@5.1.1 --save
> npm install redux-form --save

At the time of writing this article, redux-form didn't play well with react-redux version 6 so for this article we installed version 5. create a folder in the src called redux to hold all the redux related stuff. In that folder create a file called reducers.js with the following:

import {combineReducers} from 'redux';
import {reducer as formReducer} from 'redux-form'

export default combineReducers({
    form: formReducer
});

This file will combine all the reducers for our app to a single root reducer. Our app doesn't have any reducers. We do however need to connect the reducer that is shipped in the redux-form package. we import it and place it in the combineReducers, when your app grows and you will add more reducers you will have to import them in this file and connect them in the combineReducers. Time to create our store. In the redux folder create another file named: store.js with the following:

import {createStore} from 'redux';
import rootReducer from './reducers';

export default createStore(rootReducer);

We pass our reducer in the createStore method to create our redux store. This will also be the place to install middlewares. We would also want to view our state when modifying our form, to do so we will have to install the redux dev tools in order to view our state. Let's install the redux-devtools-extension package. In the terminal type:

> npm install redux-devtools-extension --save-dev

Now modify the store file store.js to connect to the redux devtools.

import {createStore} from 'redux';
import rootReducer from './reducers';
import {devToolsEnhancer} from 'redux-devtools-extension';

export default createStore(rootReducer, devToolsEnhancer());

Now we will need to wrap our application with the Provider component. in the entry point file index.js modify the code to use the Provider with our store.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import store from './redux/store';
import {Provider} from 'react-redux';

ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();

We simply wrapped our app with the Provider component with our store in order to provide our store to components that will require it. We can now start creating our form using redux-form. In the src folder create another file called ReduxFromRegister.js with the following code.

import React, {Component} from 'react';
import {reduxForm, SubmissionError, Field, Form} from 'redux-form';
import * as Yup from 'yup';

const RegisterSchema = Yup.object().shape({
    username: Yup.string()
        .max(20, 'Too long')
        .required('This field is required'),
    email: Yup.string()
        .email('Invalid mail')
        .required('This field is required'),
    fullName: Yup.string()
        .required('This field is required')
        .test('num-words', 'You must supply 2 words', function(value) {
            if (value && value.split(' ').length === 2) {
                return true;
            }
            return false;
        }),
    password: Yup.string()
        .required('This field is required')
        .min(5, 'Too short!'),
    repeat: Yup.string()
        .required('This field is required')
        .min(5, 'Too short!')
        .oneOf([Yup.ref('password'), null])
});

class ReduxFormRegister extends Component {
    async mySubmitFunction(values)  {
        try {
            await RegisterSchema.validate(values, {abortEarly: false})
            alert('success');
        } catch(err) {            
            const errors = {};
            for(let error of err.inner) {
                errors[error.path] = error.errors[0];
            }
            throw new SubmissionError(errors);
        }        
    }

    renderField = ({ input, label, type, meta: { touched, error } }) => {
        return (
            <div className="form-group">
                <label>{label}</label>
                <input {...input} type={type} className="form-control" />
                {
                    touched && error && (<div className="alert alert-danger">{error}</div>)
                }
            </div>
        )
    }

    render() {
        return (
            <Form noValidate onSubmit={ this.props.handleSubmit(this.mySubmitFunction) }>
                <Field
                    label="Username"
                    name="username"
                    type="text"
                    component={this.renderField} />
                <Field
                    label="Email"
                    name="email"
                    type="email"
                    component={this.renderField} />
                <Field
                    label="Full Name"
                    name="fullName"
                    type="text"
                    component={this.renderField} />
                <Field
                    label="Password"
                    name="password"
                    type="password"
                    component={this.renderField} />
                <Field
                    label="Repeat Password"
                    name="repeat"
                    type="password"
                    component={this.renderField} />
                <div className="form-group">
                    <button 
                        className="btn btn-primary" 
                        type="submit">Submit</button>
                </div>
            </Form>
        );
    }
}

export default reduxForm({
    form: 'register'
})(ReduxFormRegister);

We are using Yup here as well with a similar validation like before. In redux-form there is no built in yup schema to enter in the props so we will have to manually use our schema in our submit function and throw SubmissionError is the validation is failing. The form we are creating here will be inside our redux state so we will have a section called form and a key of register with the state of our register form. I see no added values for using this way rather then Formik, and personally I think dealling with local components forms in the redux global state although is possible it is essentially wrong plus is hurting our app performance.

Summary

Dealling with forms is going to be a big part of almost all web applications. React being a library with a very specific set of targets, are implementing a very basic tools for dealling with forms and validation. So in order to ease our way we will have to combine another library to help us with the forms. In this article we saw the 2 most popular options which are redux-form and Formik. Both are excellent choices and will do the job properly. personally I tend to favor Formik a bit more, being more light weight and fast, and being unopioniated regarding using Redux, or Flux or Mobx. Seems like the solution of dealing with forms inside the component and not in the global state is a bit more fitting. We also recommend on using Yup as the tool to use to create validation schemas. Try and experiment with Formik or redux-form in your next react app. Happy coding...