DEV Community

Agoi Abel Adeyemi
Agoi Abel Adeyemi

Posted on • Edited on

28 7

The complete guide to Forms in React

a letter about react forms to me in the future

Forms are very useful in any web application. Unlike angular and angularjs, that gives form validation out of the box. You have to handle forms yourself in React. This brought about many complications like how to get form values, how do I manage form state, how do I validate my form on the fly and show validation messages. There are different methods and libraries out there to help with this but if you are like me that hates dependent on too many libraries, welcome on board, We are going to bootstrap our own form from the ground up.

There are two types of form input in react. We have the uncontrolled input and the controlled input. The uncontrolled input are like traditional HTML form inputs, they remember what you typed. We will use ref to get the form values.

submitFormHandler = event => {
event.preventDefault();
console.dir(this.refs.name.value); //will give us the name value
}
render() {
return (
<div>
<form onSubmit={this.submitFormHandler}>
<div>
<input type="text" name="name" ref="name" />
</div>
</form>
</div>
);
}

We added the ref="name" to the input tag so that we can access the value with this.refs.name.value when the form is submitted. The downside to this is that you have to “pull” the value from the field when you need it and this can happen when the form is submitted.

The controlled input is when the react component that renders a form also controls what happens in that form on subsequent user input. Meaning, as form value changes, the component that renders the form saves the value in its state.

import React, { Component } from 'react';
class FormComponent extends Component {
constructor () {
this.state = {
email: ''
}
}
changeHandler = event => {
this.setState({
email: event.target.value
});
}
render () {
return (
<form>
<input type="email"
name="email"
value={this.state.email}
onChange={this.changeHandler}
/>
</form>
);
}
}
export default FormComponent;
view raw form_01.js hosted with ❤ by GitHub

Of course, another component can handle the form state. The goal is that each time the input changes, the method changeHandler is called and will store the input state. Hence the component always has the current value of the input without needing to ask for it. This means that the form component can respond to input changes immediately; for example

  • in-place feedback, like validation
  • disabling the button unless all fields have valid data
  • enforcing a specific input format

Handling multiple forms inputs

In most situations, we are going to have more than one form input. We need a way to capture the input with a method instead of declaring multiple methods to do this. Hence we are going to modify the changeHandler to below:

changeHandler = event => {
const name = event.target.name;
const value = event.target.value;
this.setState({
formControls: {
[name]: value
}
});
}

Because of the way the changeHandler has been modified above, our form input can reference it to update it states dynamically.

import React, { Component } from 'react';
class FormContainer extends Component {
constructor () {
this.state = {
formControls: {
email: {
value: ''
},
name: {
value: ''
},
password: {
value: ''
}
}
}
}
changeHandler = event => {
const name = event.target.name;
const value = event.target.value;
this.setState({
formControls: {
...this.state.formControls,
[name]: {
...this.state.formControls[name],
value
}
}
});
}
render() {
return (
<form>
<input type="email"
name="email"
value={this.state.formControls.email.value}
onChange={this.changeHandler}
/>
<input type="text"
name="name"
value={this.state.formControls.name.value}
onChange={this.changeHandler}
/>
<input type="password"
name="password"
value={this.state.formControls.password.value}
onChange={this.changeHandler}
/>
</form>
);
}
}
export default FormContainer;

Create a TextInput Component

There are different input elements e.g text, email, password, select option, checkbox, date, radio button, etc. I love to create a separate custom component for the input elements, let us start with the text input type.

import React from 'react';
import './style.css';
const TextInput = (props) => {
return (
<div className="form-group">
<input type="text" className="form-control" {...props} />
</div>
);
}
export default TextInput;
view raw TextInput.js hosted with ❤ by GitHub

Notice the {…props}, we use this to distribute the props to the input element. We can use the custom text input element like below:

import React, { Component } from 'react';
import TextInput from './TextInput';
class FormComponent extends Component {
constructor() {
super();
this.state = {
formControls: {
name: {
value: '',
placeholder: 'What is your name'
}
}
}
}
changeHandler = event => {
const name = event.target.name;
const value = event.target.value;
this.setState({
formControls: {
[name]: value
}
});
}
render() {
return (
<TextInput name="name"
placeholder={this.state.formControls.name.placeholder}
value={this.state.formControls.name.value}
onChange={this.changeHandler}
/>
);
}

Validating our Custom TextInput

Since we are using the controlled input, we can add more keys to our formControls state to help validate the input. We need the valid property to denote if the input is valid or not, the validationRules contains the list of the rules to be checked before the input is valid.

constructor() {
super();
this.state = {
formControls: {
name: {
value: ''.
placeholder: 'What is your name',
valid: false,
touched: false,
validationRules: {
minLength: 3
}
}
}
}
}

Our aim is that each time the input changes. We make sure the validationRules for that input is checked for true or false, then update the valid key with the result of the check. We also added the touched property to denote that the user has touched the form input, this will help with displaying validation feedback when the input has been touched. The check will be done in the changeHandler method like below:

changeHandler = event => {
const name = event.target.name;
const value = event.target.value;
const updatedControls = {
...this.state.formControls
};
const updatedFormElement = {
...updatedControls[name]
};
updatedFormElement.value = value;
updatedFormElement.touched = true;
updatedFormElement.valid = validate(value, updatedFormElement.validationRules);
updatedControls[name] = updatedFormElement;
this.setState({
formControls: updatedControls
});
}

The valid is equated to the methodvalidate(value, prevState.formControls[name]).validationRules) which we will use to check if the valid status for a particular control is true or false.

const validate = (value, rules) => {
let isValid = true;
for (let rule in rules) {
switch (rule) {
case 'minLength': isValid = isValid && minLengthValidator(value, rules[rule]); break;
default: isValid = true;
}
}
return isValid;
}
/**
* minLength Val
* @param value
* @param minLength
* @return
*/
const minLengthValidator = (value, minLength) => {
return value.length >= minLength;
}
export default validate;
view raw validation.js hosted with ❤ by GitHub

I move the validate method to a separate class then import it. The validate method accepts two parameters, the value and the rules. We loop through the rules and check if each rule is valid, then return true when valid and false when invalid.

Let assume we want to add another validation on name e.g we wantname to be required. All we need do is update the formControl for name validationRules, and write the logic for it in the validator class like below

constructor() {
super();
this.state = {
formControls: {
name: {
value: ''.
placeholder: 'What is your name',
valid: false,
touched: false,
validationRules: {
minLength: 3,
isRequired: true //just added this
}
}
}
}
}

Then we need to update the validator class to accommodate for the required validator.

const validate = (value, rules) => {
let isValid = true;
for (let rule in rules) {
switch (rule) {
case 'minLength': isValid = isValid && minLengthValidator(value, rules[rule]); break;
case 'isRequired': isValid = isValid && requiredValidator(value); break;
case 'isEmail': isValid = isValid && emailValidator(value); break;
default: isValid = true;
}
}
return isValid;
}
/**
* minLength Val
* @param value
* @param minLength
* @return
*/
const minLengthValidator = (value, minLength) => {
return value.length >= minLength;
}
/**
* Check to confirm that feild is required
*
* @param value
* @return
*/
const requiredValidator = value => {
return value.trim() !== '';
}
/**
* Email validation
*
* @param value
* @return
*/
const emailValidator = value => {
var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(value).toLowerCase());
}
export default validate;

We created a custom TextInput, we created a formControl that has a property named name with a validation rules of isRequired and minLength of 3. Below is the component that handles this:

import React, { Component } from 'react';
import TextInput from './TextInput';
import validate from './validator';
class FormComponent extends Component {
constructor() {
super();
this.state = {
formControls: {
name: {
value: ''.
placeholder: 'What is your name',
valid: false,
touched: false,
validationRules: {
minLength: 3,
isRequired: true
}
}
}
}
}
changeHandler = event => {
const name = event.target.name;
const value = event.target.value;
const updatedControls = {
...this.state.formControls
};
const updatedFormElement = {
...updatedControls[name]
};
updatedFormElement.value = value;
updatedFormElement.touched = true;
updatedFormElement.valid = validate(value, updatedFormElement.validationRules);
updatedControls[name] = updatedFormElement;
this.setState({
formControls: updatedControls
});
}
formSubmitHandler = () => {
console.dir(this.state.formControls);
}
render() {
return (
<div>
<TextInput name="name"
placeholder={this.state.formControls.name.placeholder}
value={this.state.formControls.name.value}
onChange={this.changeHandler}
/>
<button onClick={this.formSubmitHandler}> Submit </button>
</div>
);
}
}

If we click the submit button after filling the TextInput, the formSubmitHandler will console the formControls value like below

valid = true or false

The good thing is that we do not have to wait till the user click submit before we can know if the form input is valid or not. Since it is actually stored in the component state, so, therefore, we can use this to display error message or feedback when the user is typing. We can even disable the submit button until the validation passes.

Displaying error feedback

For us to be able to display error feedback on the input, we need to pass the touched and valid property for that particular input as a prop to it component. We will add the error style based on the valid status and we want to do this only when the input has been touched.

render() {
return (
<TextInput name="name"
placeholder={this.state.formControls.name.placeholder}
value={this.state.formControls.name.value}
onChange={this.changeHandler}
touched={this.state.formControls.name.touched}
valid={this.state.formControls.name.valid}
/>
);
}

We also need to modify our TextInput component to display the style based on the value props.valid and props.touched.

import React from 'react';
const TextInput = props => {
let formControl = "form-control";
if (props.touched && !props.valid) {
formControl = 'form-control control-error';
}
return (
<div className="form-group">
<input type="text" className={formControl} {...props} />
</div>
);
}
export default TextInput;

Please note that you should have added the form-control and control-error style into the App.css.

.form-control {
width: 50%;
border: 1px solid #ccc;
padding: 10px;
}
.control-error {
border: 1px solid red;
}
view raw App.css hosted with ❤ by GitHub

You should see a screenshot like below if you TextInput is invalid and had been touched.

Disable Submit Button if the form is Invalid

Html 5 has a disabled property on button input, we can equate our formControls property valid status to the disabled property. As long as the formControls is not valid.

<button onClick={this.formSubmitHandler}
disabled={!this.state.formControls.name.valid}
>
Submit
</button>

The disabled={!this.state.formControls.name.valid} will work fine if we have only one form control but if we need to handle more than one form control, we can set a new property to the state which will keep track of when the validity status of the whole formControl object. So we need to update our state to accommodate for this

constructor () {
super();
this.state = {
formIsValid: false, //we will use this to track the overall form validity
formControls: {
name: {
value: '',
valid: false,
validationRules: {
isRequired: true
},
placeholderText: 'Your name please',
touched: false
}
}
};
}

We need to update the changeHandler method so we can loop through all form controls valid status, and when valid, update the formIsValid status to true.

changeHandler = event => {
const name = event.target.name;
const value = event.target.value;
const updatedControls = {
...this.state.formControls
};
const updatedFormElement = {
...updatedControls[name]
};
updatedFormElement.value = value;
updatedFormElement.touched = true;
updatedFormElement.valid = validate(value, updatedFormElement.validationRules);
updatedControls[name] = updatedFormElement;
let formIsValid = true;
for (let inputIdentifier in updatedControls) {
formIsValid = updatedControls[inputIdentifier].valid && formIsValid;
}
this.setState({
formControls: updatedControls,
formIsValid: formIsValid
});
}

With this setup, it will be easier for us to set the disabled property to formIsValid status, and this will handle one or more form object.

<button onClick={this.formSubmitHandler}
disabled={!this.state.formIsValid}
>
Submit
</button>

Considering other form input type

TEXTAREA: The textarea, email and password will work similar to a text input. We can create a TextArea component.

import React from 'react';
const TextArea = props => {
let formControl = "form-control";
if (props.touched && !props.valid) {
formControl = 'form-control control-error';
}
return (
<div className="form-group">
<textarea {...props} className={formControl} />
</div>
);
}
export default TextArea;
view raw TextArea.js hosted with ❤ by GitHub

EMAIL: We can also create an Email component just like the TextInput

import React from 'react';
const Email = props => {
let formControl = "form-control";
if (props.touched && !props.valid) {
formControl = 'form-control control-error';
}
return (
<div className="form-group">
<input type="email" className={formControl} {...props} />
</div>
);
}
export default Email;
view raw email.js hosted with ❤ by GitHub

PASSWORD: We can also create a Password component just like the TextInput

import React from 'react';
const Password = props => {
let formControl = "form-control";
if (props.touched && !props.valid) {
formControl = 'form-control control-error';
}
return (
<div className="form-group">
<input type="password" className={formControl} {...props} />
</div>
);
}
export default Password;
view raw Password.js hosted with ❤ by GitHub

The email, textarea and password form control will look similar to the text input form input

constructor() {
super();
this.state = {
formControls: {
age: {
value: ''.
placeholder: 'What is your age',
valid: false,
touched: false,
validationRules: {
minLength: 3,
isRequired: true
}
}
}
}
}
view raw form_control.js hosted with ❤ by GitHub

SELECT OPTION: The Select Option form control is slightly different to the other form control because we have to accommodate for the select options. It will look like to below

gender: {
value: '',
placeholder: 'What is your gender',
valid: false,
touched: false,
validationRules: {
isRequired: true,
},
options: [
{ value: 'male', displayValue: 'Male' },
{ value: 'female', displayValue: 'Female'}
]
}

then the Select Option component will look like below

import React from 'react';
const Select = props => {
let formControl = "form-control";
if (props.touched && !props.valid) {
formControl = 'form-control control-error';
}
return (
<div className="form-group">
<select className={formControl} value={props.value} onChange={props.onChange} name={props.name}>
{props.options.map(option => (
<option value={option.value}>
{option.displayValue}
</option>
))}
</select>
</div>
);
}
export default Select;
view raw Select.js hosted with ❤ by GitHub

RADIO: The radio input is similar to the select option since it’s only one option that can be selected out of the available options, The form control will be similar to the select option form control. Below is how the radio button component looks like.

import React from 'react';
const Radio = props => {
let formControl = "form-control";
if (props.touched && !props.valid) {
formControl = 'form-control control-error';
}
return (
<div className="form-group">
{props.options.map(option => (
<div className="form-group" key={option.value}>
<label>{option.displayValue}</label>
<input type="radio"
name={props.name}
value={option.value}
onChange={props.onChange}
className={formControl}
/>
</div>
))}
</div>
);
}
export default Radio;
view raw Radio.js hosted with ❤ by GitHub

Putting it all together, Assuming we want to have an email input, name (TextInput), gender (Select Input), radio input all in a form control. Below is an example of what your component will look like

import React, { Component } from 'react';
import './App.css';
import TextInput from './TextInput';
import validate from './validate';
import TextArea from './TextArea';
import Email from './Email';
import Select from './Select';
import Radio from './Radio';
class App extends Component {
constructor() {
super();
this.state = {
formIsValid: false,
formControls: {
name: {
value: '',
placeholder: 'What is your name',
valid: false,
validationRules: {
minLength: 4,
isRequired: true
},
touched: false
},
address: {
value: '',
placeholder: 'What is your address',
valid: false,
validationRules: {
minLength: 4,
isRequired: true
},
touched: false
},
my_email: {
value: '',
placeholder: 'What is your email',
valid: false,
validationRules: {
isRequired: true,
isEmail: true
},
touched: false
},
gender: {
value: '',
placeholder: 'What is your gender',
valid: false,
touched: false,
validationRules: {
isRequired: true,
},
options: [
{ value: 'male', displayValue: 'Male' },
{ value: 'female', displayValue: 'Female'}
]
},
my_radio: {
value: '',
placeholder: 'Are you a frontend developer',
valid: false,
touched: false,
validationRules: {
// isRequired: true,
},
options: [
{ value: 0, displayValue: 'No' },
{ value: 1, displayValue: 'Yes' }
]
}
}
}
}
changeHandler = event => {
const name = event.target.name;
const value = event.target.value;
const updatedControls = {
...this.state.formControls
};
const updatedFormElement = {
...updatedControls[name]
};
updatedFormElement.value = value;
updatedFormElement.touched = true;
updatedFormElement.valid = validate(value, updatedFormElement.validationRules);
updatedControls[name] = updatedFormElement;
let formIsValid = true;
for (let inputIdentifier in updatedControls) {
formIsValid = updatedControls[inputIdentifier].valid && formIsValid;
}
this.setState({
formControls: updatedControls,
formIsValid: formIsValid
});
}
formSubmitHandler = () => {
const formData = {};
for (let formElementId in this.state.formControls) {
formData[formElementId] = this.state.formControls[formElementId].value;
}
console.dir(formData);
}
render() {
return (
<div className="App">
<TextInput name="name"
placeholder={this.state.formControls.name.placeholder}
value={this.state.formControls.name.value}
onChange={this.changeHandler}
touched={this.state.formControls.name.touched}
valid={this.state.formControls.name.valid}
/>
<TextArea name="address"
placeholder={this.state.formControls.address.placeholder}
value={this.state.formControls.address.value}
onChange={this.changeHandler}
touched={this.state.formControls.address.touched}
valid={this.state.formControls.address.valid}
/>
<Email name="my_email"
placeholder={this.state.formControls.my_email.placeholder}
value={this.state.formControls.my_email.value}
onChange={this.changeHandler}
touched={this.state.formControls.my_email.touched}
valid={this.state.formControls.my_email.valid}
/>
<Select name="gender"
value={this.state.formControls.gender.value}
onChange={this.changeHandler}
options={this.state.formControls.gender.options}
touched={this.state.formControls.gender.touched}
valid={this.state.formControls.gender.valid}
/>
<Radio name="my_radio"
value={this.state.formControls.my_radio.value}
onChange={this.changeHandler}
options={this.state.formControls.my_radio.options}
touched={this.state.formControls.my_radio.touched}
valid={this.state.formControls.my_radio.valid}
/>
<button onClick={this.formSubmitHandler}
disabled={! this.state.formIsValid}
>
Submit
</button>
</div>
);
}
}
export default App;

Thanks for reading.

Image of AssemblyAI tool

Transforming Interviews into Publishable Stories with AssemblyAI

Insightview is a modern web application that streamlines the interview workflow for journalists. By leveraging AssemblyAI's LeMUR and Universal-2 technology, it transforms raw interview recordings into structured, actionable content, dramatically reducing the time from recording to publication.

Key Features:
🎥 Audio/video file upload with real-time preview
🗣️ Advanced transcription with speaker identification
⭐ Automatic highlight extraction of key moments
✍️ AI-powered article draft generation
📤 Export interview's subtitles in VTT format

Read full post

Top comments (3)

Collapse
 
antonio_pangall profile image
Antonio Pangallo

Hi Agoi, I would suggestyou to have a look into github.com/iusehooks/usetheform .

Collapse
 
jitheshkt profile image
Jithesh. KT

I use reactstrap validation module. Its so far the biggest life and time saver I came across when working with react.

availity.github.io/availity-reacts...

Collapse
 
abelagoi profile image
Agoi Abel Adeyemi

Okay, I will check it out too. Thanks

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay