Controlled Forms with Frontend Validations using React-Bootstrap
Alec Grey
Posted on January 29, 2021
I've been working on my capstone project for the last couple of weeks, and with it I've had a chance to learn a lot more about react-bootstrap for putting together functional, and aesthetically pleasing web pages. One place that this framework has really helped me to up my game is in creating responsive forms. Pairing with React hooks, you can very easily make forms that store input in state, keep control form values, and display invalidations when necessary. Lets create a simple form with react & react-bootstrap to see how it's done!
App Setup
We're going to build a simple form with a few fields. To start, lets initialize our app with npx create-react-app form-demo
. Next we're going to add react-bootstrap to our project with either npm install --save react-bootstrap
or yarn add react-bootstrap
.
Because React-Bootstrap comes with specific out-of-the-box styling, it is also helpful to add vanilla-bootstrap for additional customization. To do this, start with either npm install --save bootstrap
, or yarn add bootstrap
, then import it into your index.js or App.js files:
// ./src/App.js
// ...other imports
import 'bootstrap/dist/css/bootstrap.min.css';
Now that our app is set up, we can start building out our basic form.
Form Building with React-Bootstrap
Like all components, we need to use import
in order to bring them in for availability in our app. Now that we have the library installed, we can easily add react-bootstrap components to our app:
// ./src/App.js
// ...other imports
import Form from 'react-bootstrap/Form';
This convention is consistent throughout the library, but I highly suggest reviewing the documentation for specific import instructions.
Building the form follows very straightforward convention, but also keeps room open for styling choices to be mixed in. Here is the code for our form, which will be used to review food items at a restaurant:
const App = () => {
return (
<div className='App d-flex flex-column align-items-center'>
<h1>How was your dinner?</h1>
<Form style={{ width: '300px' }}>
<Form.Group>
<Form.Label>Name</Form.Label>
<Form.Control type='text'/>
</Form.Group>
<Form.Group>
<Form.Label>Food?</Form.Label>
<Form.Control as='select'>
<option value=''>Select a food:</option>
<option value='chicken parm'>Chicken Parm</option>
<option value='BLT'>BLT</option>
<option value='steak'>Steak</option>
<option value='salad'>Salad</option>
</Form.Control>
</Form.Group>
<Form.Group>
<Form.Label>Rating</Form.Label>
<Form.Control type='number'/>
</Form.Group>
<Form.Group>
<Form.Label>Comments</Form.Label>
<Form.Control as='textarea'/>
</Form.Group>
<Button type='submit'>Submit Review</Button>
</Form>
</div>
)
}
Lets break this down:
- Following React convention, we have the div wrapping the rest of the component.
- We wrap the entire form in a single
Form
component - Each field is grouped using the
Form.Group
component wrapper. This generally follows a 1:1 rule for Group:Field, but there are advanced cases such as having multiple fields on a single row where you could wrap multiple fields. - Use
Form.Label
for labelling each field. You can use added styling on the form group in order to make this display inline with your form input, but by default they will stack vertically. - Use
Form.Control
to designate the input field. Here we have a couple of options for inputs. If your field resembles an HTML input tag, you can usetype='type'
to determine what type of input field it will be. In our example we usetype='text'
andtype='number'
. If you will be using another HTML tag, such as a<select>
tag, you can use theas='tag'
designation to determine what you get. In our example we use both anas='select'
and anas='textarea'
to designate these. - To submit the form, we add a button to the bottom with a
type='submit'
designation. Personally, I prefer not to use the 'submit' type, as we will more than likely be overriding the default submit procedure anyway.
As you can see, we can very quickly build a form that is aesthetically pleasing, but the important next step is to make it functional!
Updating State with Form Input
Using react hooks, we're going to create 2 pieces of state: the form
and the errors
.
const [ form, setForm ] = useState({})
const [ errors, setErrors ] = useState({})
The form
object will hold a key-value pair for each of our form fields, and the errors
object will hold a key-value pair for each error that we come across on form submission.
To update the state of form
, we can write a simple function:
const setField = (field, value) => {
setForm({
...form,
[field]: value
})
}
This will update our state to keep all the current form values, then add the newest form value to the right key location.
We can now add callback functions for onChange
on each form field:
// do for each Form.Control:
<Form.Label>Name</Form.Label>
<Form.Control type='text' onChange={ e => setField('name', e.target.value) }/>
As you can see, we are setting the key of 'name' to the value of the input field. If your form will be used to create a new instance in the backend, it is a good idea to set the key to the name of the field that it represents in the database.
Great! Now we have a form that updates a state object when you change the value. Now what about when we submit the form?
Checking for errors on submit
We now need to check our form for errors! Think about what we don't want our backend to receive as data, and come up with your cases. In our form, we don't want
- Blank or null values
- Name must be less than 30 characters
- Ratings above 5 or less than 1
- Comments greater than 100 characters
Using these cases, we're going to create a function that checks for them, then constructs an errors
object with error messages:
const findFormErrors = () => {
const { name, food, rating, comment } = form
const newErrors = {}
// name errors
if ( !name || name === '' ) newErrors.name = 'cannot be blank!'
else if ( name.length > 30 ) newErrors.name = 'name is too long!'
// food errors
if ( !food || food === '' ) newErrors.food = 'select a food!'
// rating errors
if ( !rating || rating > 5 || rating < 1 ) newErrors.rating = 'must assign a rating between 1 and 5!'
// comment errors
if ( !comment || comment === '' ) newErrors.comment = 'cannot be blank!'
else if ( comment.length > 100 ) newErrors.comment = 'comment is too long!'
return newErrors
}
Perfect. Now when we call this, we will be returned an object with all the errors in our form.
Let's handle submit now, and check for errors. Here is our order of operations:
- Prevent default action for a form using
e.preventDefault()
- Check our form for errors, using our new function
- If we receive errors, update our state accordingly, otherwise proceed with form submission!
now to handle submission:
const handleSubmit = e => {
e.preventDefault()
// get our new errors
const newErrors = findFormErrors()
// Conditional logic:
if ( Object.keys(newErrors).length > 0 ) {
// We got errors!
setErrors(newErrors)
} else {
// No errors! Put any logic here for the form submission!
alert('Thank you for your feedback!')
}
}
By using Object.keys(newErrors).length > 0
we are simply checking to see if our object has any key-value pairs, or in other words, did we add any errors.
Now that we have errors, we need to display them in our form! This is where we will add our last bit of React-Bootstrap spice: Form.Control.Feedback
.
Setting Invalidations and Feedback
React bootstrap allows us to add a feedback field, and to tell it what and when to display information.
On each of our forms, we will add an isInvalid
boolean, and a React-Bootstrap Feedback component tied to it:
<Form.Group>
<Form.Label>Name</Form.Label>
<Form.Control
type='text'
onChange={ e => setField('name', e.target.value) }
isInvalid={ !!errors.name }
/>
<Form.Control.Feedback type='invalid'>
{ errors.name }
</Form.Control.Feedback>
</Form.Group>
With this added, Bootstrap will highlight the input box red upon a true value for isInvalid
, and will display the error in Form.Control.Feedback
.
There's one final step however! We need to reset our error fields once we have addressed the errors. My solution for this is to update the errors object in tandem with form input, like so:
const setField = (field, value) => {
setForm({
...form,
[field]: value
})
// Check and see if errors exist, and remove them from the error object:
if ( !!errors[field] ) setErrors({
...errors,
[field]: null
})
}
Now, when a new input is added to the form, we will reset the errors at that place as well. Then on the next form submission, We can check for errors again!
Final product in action:
Thanks for reading! I hope this was helpful.
Posted on January 29, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.