Picking up from where I left off yesterday, I'm going to be working on client-side form validation. For now, I'm going to do it without a form library, but in the long run it's probably better to use a library, so I'm planning to do a second pass with Formik.
The first thing I noticed when I got started today was that I made a mistake when creating the password confirmation field; I didn't change its name. Also, the example form I copied from Reactstrap didn't have a name attribute on the checkbox, so I added that as well.
// Survey.jsx
...
<FormGroup>
<Label for="exampleConfirm">Confirm</Label>
<Input
id="exampleConfirm"
name="confirm"
placeholder="Type password again"
type="password"
/>
</FormGroup>
...
<FormGroup check>
<Input type="checkbox" name="check" />
{' '}
<Label check>Check me out</Label>
</FormGroup>
...
Bottom Padding
One thing I noticed yesterday but didn't take the time to fix was the lack of padding at the bottom of the page, which was causing the submit button to sit right at the bottom edge:
The fix was just a Bootstrap padding class on the container:
// App.jsx
...
<Container className="pb-5">
...
Unrecognized Prop
As an aside: this may be obvious to most, but I've been working with Node a lot and wasn't expecting it. I set up a dummy submit handler that logged to the console, but it wasn't showing up in terminal. This is because React console logs go to the browser console, not terminal. 🤦♀️
As I was checking the console to make sure the submit handler was working (it was!) I noticed this error:
I recognized this as coming from the NavLink element; activeClassName
was part of the code I found for getting Reactstrap and React Router to work together. After some research, it appears that the error is actually harmless and can be safely ignored, but I tried removing the prop and found that it's not actually necessary; the navigation works without it.
//Header.jsx
...
<Nav pills>
<NavItem>
<NavLink tag={RRNavLink} to="/">Home</NavLink>
</NavItem>
<NavItem>
<NavLink tag={RRNavLink} to="/survey">Survey</NavLink>
</NavItem>
<NavItem>
<NavLink tag={RRNavLink} to="/dashboard">Dashboard</NavLink>
</NavItem>
</Nav>
...
No Favicon
Another error I spotted in the console was this:
I didn't understand this one because the file favicon.ico was automatically created in the public folder by
create-react-app
, so I expected it to be set up out of the box. Turns out it was a browser caching issue from previously running a different project on localhost:3000
that didn't have a favicon. Clearing my Firefox web cache fixed the issue.
While I was at it, I took a bit of time to make a custom favicon so that I can easily spot my development site among all the tabs I have open. 😝
Form Handling
If you're looking for an introduction to React, functional components and hooks, or React forms, I highly, highly recommend Bob Ziroll's free React course on Scrimba.
Now finally, on to the forms. Before getting into validation, I'm setting up the basic form handling with React. Fortunately, it seems that Reactstrap forms and inputs function exactly the same as their native DOM counterparts, so I was able to use what I already know about React forms.
The onSubmit handler just console logs the form data for now, since it's not hooked up to anything.
There were two input types I wasn't familiar with, however: file and multi-select. File gave me a bit of trouble at first because I didn't realize it's an uncontrolled component. For multi-select, it was easiest to make a separate onChange handler, as both select and multi-select have the same type (select
). I probably could have checked for the multiple
attribute instead, but the separate handler seemed cleaner to me.
// Survey.jsx
import { useState } from 'react';
import {
Row,
Col,
Form,
FormGroup,
Label,
Input,
FormText,
Button
} from 'reactstrap';
export default function Survey() {
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
confirm: "",
select: 1,
selectMulti: [],
text: "",
file: undefined,
radio1: "",
check: false
})
function handleSubmit(e) {
e.preventDefault();
console.log(formData);
}
function handleChange(e) {
const { name, value, type, checked, files } = e.target;
let inputValue;
if (type === 'checked') inputValue = checked;
else if (type === 'flie') inputValue = files[0];
else inputValue = value;
setFormData(prevData => {
return {
...prevData,
[name]: inputValue
}
});
}
function handleChangeMulti(e) {
const { name } = e.target;
const updatedOptions =
[...e.target.options]
.filter(option => option.selected)
.map(option => option.value);
setFormData(prevData => {
return {
...prevData,
[name]: updatedOptions
}
});
}
return (
<Col>
<h1>Survey</h1>
<Form onSubmit={handleSubmit}>
<Row>
<Col>
<FormGroup>
<Label for="exampleName">Name</Label>
<Input
id="exampleName"
name="name"
placeholder="Name"
type="text"
value={formData.name}
onChange={handleChange}
/>
</FormGroup>
</Col>
<Col>
<FormGroup>
<Label for="exampleEmail">Email</Label>
<Input
id="exampleEmail"
name="email"
placeholder="Email"
type="email"
value={formData.email}
onChange={handleChange}
/>
</FormGroup></Col></Row>
<Row>
<Col>
<FormGroup>
<Label for="examplePassword">Password</Label>
<Input
id="examplePassword"
name="password"
placeholder="Password"
type="password"
value={formData.password}
onChange={handleChange}
/>
</FormGroup>
</Col>
<Col>
<FormGroup>
<Label for="exampleConfirm">Confirm</Label>
<Input
id="exampleConfirm"
name="confirm"
placeholder="Type password again"
type="password"
value={formData.confirm}
onChange={handleChange}
/>
</FormGroup>
</Col>
</Row>
<Row>
<Col>
<FormGroup>
<Label for="exampleSelect">Select</Label>
<Input
id="exampleSelect"
name="select"
type="select"
value={formData.select}
onChange={handleChange}
>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</Input>
</FormGroup>
</Col>
<Col>
<FormGroup>
<Label for="exampleSelectMulti">Select Multiple</Label>
<Input
id="exampleSelectMulti"
multiple
name="selectMulti"
type="select"
value={formData.selectMulti}
onChange={handleChangeMulti}
>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</Input>
</FormGroup>
</Col>
<Col>
<FormGroup>
<Label for="exampleText">Text Area</Label>
<Input
id="exampleText"
name="text"
type="textarea"
value={formData.text}
onChange={handleChange}
/>
</FormGroup>
</Col>
</Row>
<FormGroup>
<Label for="exampleFile">File</Label>
<Input
id="exampleFile"
name="file"
type="file"
onChange={handleChange}
/>
<FormText>
This is some placeholder block-level help text for the above input. It's a bit lighter and easily wraps to a new line.
</FormText>
</FormGroup>
<FormGroup tag="fieldset">
<legend>Radio Buttons</legend>
<FormGroup check>
<Input
name="radio1"
type="radio"
value="one"
checked={formData.radio1 === "one"}
onChange={handleChange}
/>
{' '}
<Label check>
Option one is this and that—be sure to include why it's great
</Label>
</FormGroup>
<FormGroup check>
<Input
name="radio1"
type="radio"
value="two"
checked={formData.radio1 === "two"}
onChange={handleChange}
/>
{' '}
<Label check>
Option two can be something else and selecting it will deselect option one
</Label>
</FormGroup>
<FormGroup
check
disabled
>
<Input
name="radio1"
type="radio"
value="three"
checked={formData.radio1 === "three"}
onChange={handleChange}
/>
{' '}
<Label check>
Option three is not disabled
</Label>
</FormGroup>
</FormGroup>
<FormGroup check>
<Input
type="checkbox"
name="check"
checked={formData.check}
onChange={handleChange}
/>
{' '}
<Label check>Check me out</Label>
</FormGroup>
<Button>Submit</Button>
</Form>
</Col>
)
}
Validation
Now that the form is reactified, I can do some validation.
Validation turned out to be pretty quick and easy. Since I'm just learning how validation works, I'm going to set it up on just the email for now.
A note concerning email validation: The standard way to do client-side validation of an email is to use a regex. Some regexes for email validation are too strict and will exclude valid emails, so be very careful (I'm not concerned with edge cases here, so I've used a simple regex). Additionally, no regex is entirely accurate because it can't check that the email address exists, or that it belongs to the user. If you really need to make sure the email is valid, pair your client-side regex with some server-side validation (send an email with a validation link).
It involved a couple of steps:
- Set
useState
for holding the validation state of each input. For now, I'm only setting up validation on the Email field. I'm setting it up as an object so it can easily be extended for more fields.//Survey.jsx ... const [valid, setValid] = useState({ email: "" }); ...
- Create a validation function. My function adds a valid or invalid flag to the input if it contains any text, and resets to default when empty.
//Survey.jsx ... function validateEmail(e) { const { value } = e.target; const emailRegex = /[a-z0-9]+@[a-z]+\.[a-z]{2,3}/i; if (!value.length) { setValid(prevValid => { return { ...prevValid, email: "" } }) } else if (emailRegex.test(value)) { setValid(prevValid => { return { ...prevValid, email: "valid" } }) } else { setValid(prevValid => { return { ...prevValid, email: "invalid" } }) } } ...
- Add the validation logic to the input.
the//Survey.jsx ... <FormGroup> <Label for="exampleEmail">Email</Label> <Input id="exampleEmail" name="email" placeholder="Email" type="email" value={formData.email} onChange={(e) => { handleChange(e); validateEmail(e); }} valid={valid.email === 'valid'} invalid={valid.email === 'invalid'} /> <FormFeedback>No no no!</FormFeedback> <FormFeedback valid>OK</FormFeedback> </FormGroup> ...
onChange
now has two functions, the original handler plus the validation.Valid
andinvalid
set the conditions for showing the validation, and theFormFeedback
elements set the text to be displayed when the input is valid/invalid.
Once More, With Formik (and Yup)
I feel like I've got enough practice working without a form library, so now I'm going to rewrite the form using Formik instead. After reading some of the documentation, it looks like Yup makes validation rules much easier, so I'll be using that as well. The first step is to install Formik and Yup on the client side:
npm install formik yup
Side note: I have been getting a warning about high severity vulnerabilities due to react-scripts. After some research, I've determined this is probably a false positive, so I am not looking further into the issue for now.
Formik syntax doesn't play super well with Reactstrap and although I did get it working, I had to make a few compromises. The main issue is that Formik handles validation behind the scenes (all you need to do is specify an error message) but in order for Reactstrap to style the input according to the validation status, it needs a valid
and invalid
prop. Additionally, I needed to use FormFeedback
from Reactstrap rather than ErrorMessage
from Formik, again to ensure styling was applied properly.
Note: there are two integrations for Formik and Reactstrap: reactstrap-formik
and formik-reactstrap
, however neither has been updated in 3 years so I think it's safe to say they should not be used.
I built a simpler form with 3 fields to learn how to integrate the two and it's working quite nicely:
// Survey.jsx
import { Formik, Field, Form as FForm } from 'formik';
import * as Yup from 'yup';
import {
Row,
Col,
Form,
FormGroup,
Label,
Input,
Button,
FormFeedback
} from 'reactstrap';
export default function Survey() {
return (
<Formik
initialValues={{ firstName: '', lastName: '', email: '' }}
validationSchema={Yup.object({
firstName: Yup.string()
.max(15, 'Must be 15 characters or less')
.required('Required'),
lastName: Yup.string()
.max(20, 'Must be 20 characters or less')
.required('Required'),
email: Yup.string().email('Invalid email address').required('Required'),
})}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
console.log(JSON.stringify(values, null, 2));
setSubmitting(false);
}, 1000);
}}
>
{({ errors, touched, isSubmitting }) => (
<Form tag={FForm}>
<Row>
<Col>
<FormGroup>
<Label htmlFor="firstName">First Name</Label>
<Input
tag={Field}
name="firstName"
type="text"
valid={!errors.firstName && touched.firstName}
invalid={errors.firstName && touched.firstName}
/>
<FormFeedback>{errors.firstName}</FormFeedback>
<FormFeedback valid>Valid</FormFeedback>
</FormGroup>
</Col>
<Col>
<FormGroup>
<Label htmlFor="lastName">Last Name</Label>
<Input
tag={Field}
name="lastName"
type="text"
valid={!errors.lastName && touched.lastName}
invalid={errors.lastName && touched.lastName}
/>
<FormFeedback>{errors.lastName}</FormFeedback>
<FormFeedback valid>Valid</FormFeedback>
</FormGroup>
</Col>
</Row>
<FormGroup>
<Label htmlFor="lastName">email</Label>
<Input
tag={Field}
name="email"
type="email"
valid={!errors.email && touched.email}
invalid={errors.email && touched.email}
/>
<FormFeedback>{errors.email}</FormFeedback>
<FormFeedback valid>Valid</FormFeedback>
</FormGroup>
<Button type="submit" disabled={isSubmitting}>Submit</Button>
</Form>
)}
</Formik>
);
}
- the
tag={tagName}
is the same concept I used before to get React Router and Reactstrap working together. My (limited) understanding is that I'm creating a Reactstrap component and then telling it to work like a Formik component. This lets me add Bootstrap styling while getting the Formik functionality. - the validationSchema is from Yup, which provides premade validation options that will fit most situations, but you can also make custom validations if you need them.
- Because I can't entirely rely on Formik's validation, I had to wrap the form in
({ errors, touched, isSubmitting }) => {}
, so that I have access to the error and touched (field has been active/clicked on at least once) data. This is necessary to set the valid/invalid states for Reactstrap and also to get access to the error message when the input is invalid. The isSubmitting value is set by onSubmit and is set to true when the function starts. You set it to false after submission is complete (usually this will be after a successful API post request). - The above code uses setTimeout to simulate the submission; it wouldn't normally be part of the onSubmit handler. Here I'm using it to make sure the submit button is disabled as long as isSubmitting is true (in other words, while the form is submitting). This is a good practice to make sure the user doesn't accidentally submit the form twice.
As I look ahead to what I ultimately want to do with the project, I am starting to feel that Reactstrap is going to be more trouble than it's worth. Tomorrow I think I'll rework the project to use Bootstrap classes and drop Reactstrap. Since I'm not going to continue with Reactstrap, I'm not going to bother getting it working with all the different input types.
NOTE: This is not a tutorial. I am learning how to build a MERN app and decided to record my progress. Although I'm doing my very best to make sure my code is correct, I may not always be following best practices. If you see any mistakes, feel free to contact me at l@abrocadabro.com or leave a comment. I would love to know what I can do better!