React Forms
Introduction
- Controlled Forms
- Uncontrolled Forms
- Using Formik Library
- Validation
- Creating reusable custom form elements
- Uncontrolled forms using React
- React Hook Form to create uncontrolled forms
Controlled forms
In react we can pass state management to the react component. This is what a controlled form is. It's advantages are
- Instant Feedback
- Disable controls dynamically
- Formats the input data e.g. dates 25-03-2001
Example using UseState
const [password, setPassword] = useState("");
...
<Form onSubmit={handleSubmit} className="row g-3 needs-validation">
<div>
<div className="col-md-4">
<Form.Group size="lg" controlId="password">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
value={password}
onChange={(e) => onPasswordChange(e)}
/>
<div className="invalid-feedback">{passwordError}</div>
<div className="valid-feedback">Password looks good!</div>
</Form.Group>
</div>
</div>
<div>
<div className="col-12">
<Button
type="submit"
className="btn btn-primary"
disabled={isSubmitting || !formValid}
>{`${isSubmitting ? "Logging In" : "Login"}`}</Button>
</div>
</div>
</Form>
...
Using React Components
import React from "react";
class EmailForm extends React.Component {
constructor(props) {
super(props);
}
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
handleChange(event) {
this.setState({value: event.target.value});
}
render() {
return (
<form>
<input type="email" value={this.state.value} onChange={this.handleChange} />
</form>
);
}
}
Uncontrolled Forms
Uncontrolled forms are when the DOM maintains the states and a reference is stored to it in react. To use an uncontrolled Form you need to
- Create a reference using React.createRef()
- Assign reference using ref prop on form element
- Extract value using the reference created in the constructor
import React from "react";
class TestForm extends React.Component {
constructor(props) {
super(props);
this.submit = this.submit.bind(this);
this.input = React.createRef();
}
submit(event) {
// eslint-disable-next-line no-alert
alert(`Value${this.input.current.value}`);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.submit}>
<input type="text" ref={this.input} />
<input type="submit" value="Submit" />
</form>
);
}
}
export default TestForm;
Using Formik Library
Advantages
This is what is suggests.
- Reduces Verbosity
- Reduces code for state and callbacks
- Reduces errors
- Tracks values, errors and visited fields
- Hooks up appropriate callback functions
- Helpers for sync and async validation and showing errors
- Sensible defaults
Components of Formik
Here are the components which make up a Formik form. The first being the component responsible for controlling the form
- Formik
- Form
- Field
- ErrorMessage
Internally Formik maintains three maps.
Formik Using Components
This is the example with components. This now been re-implemented using hooks. It does use styled components around the controls which I personally found to be a bit hard to debug.
import React from "react";
import styled from "styled-components";
import { Formik, Field, Form, ErrorMessage } from "formik";
const SigninForm = styled(Form)`
display: flex;
flex-direction: column;
padding: 30px;
border: 1px solid black;
`;
const Container = styled.div`
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
align-items: center;
`;
const ContentContainer = styled.div`
display: flex;
flex-direction: column;
width: 600px;
margin-top: 50px;
`;
const Title = styled.h1`
white-space: pre-line;
`;
const Label = styled.label`
margin-top: 20px;
font-size: 24px;
`;
const EmailField = styled(Field)`
height: 40px;
font-size: 24px;
`;
const ErrorLabel = styled.div`
color: red;
font-size: 26px;
`;
const PasswordField = styled(Field)`
height: 40px;
font-size: 24px;
`;
const CheckboxContainer = styled.div`
display: flex;
height: 50px;
align-items: center;
`;
const RememberMeCheckboxField = styled(Field)`
margin-top: 10px;
`;
const CheckboxLabel = styled(Label)`
margin-top: 7px;
margin-left: 10px;
`;
class LoginFormik extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = LoginFormik.handleSubmit.bind(this);
this.handleValidation = LoginFormik.handleValidation.bind(this);
}
static handleSubmit(values) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
// eslint-disable-next-line no-alert
alert(JSON.stringify(values));
}, 5000);
});
}
static handleValidation(values) {
const errors = {};
if (!values.email) {
errors.email = "Email cannot be empty";
}
if (!values.password) {
errors.password = "Password cannot be empty";
} else if (values.password.length < 8) {
errors.password = "Password must be at least 8 characters";
}
return errors;
}
render() {
return (
<Container>
<ContentContainer>
<Title>Signin Form</Title>
<Formik
initialValues={{ email: "", password: "", rememberMe: false }}
onSumbit={this.handleSumbit}
validate={this.handleValidation}
>
{() => (
<SigninForm>
<Label>Email</Label>
<EmailField name="email" type="email" />
<ErrorMessage name="email">
{(error) => <ErrorLabel>{error}</ErrorLabel>}
</ErrorMessage>
<Label>Password</Label>
<PasswordField name="password" type="password" />
<ErrorMessage name="password">
{(error) => <ErrorLabel>{error}</ErrorLabel>}
</ErrorMessage>
<CheckboxContainer>
<RememberMeCheckboxField type="checkbox" name="rememberMe" />
<CheckboxLabel>Remember Me</CheckboxLabel>
</CheckboxContainer>
</SigninForm>
)}
</Formik>
</ContentContainer>
</Container>
);
}
}
export default LoginFormik;
Formik Validation
Formik provides two types of validation and they are not a surprise
- Field level
- Form level
Field Level
Note the returning of undefined not null
...
class LoginFormik extends React.Component {
static validatePassword(value) {
if (!value) {
return "Password cannot be empty";
}
if (value.length < 5) {
return "Very Weak";
}
if (value.length < 8) {
return "Weak";
}
return undefined;
}
...
<Label>Password</Label>
<PasswordField
name="password"
type="password"
validate={LoginFormik.validatePassword}
/>
Form Level
This is done by defining a function on the Formik validate property.
<Formik
initialValues={{ email: "", password: "", rememberMe: false }}
onSumbit={this.handleSumbit}
validate={this.handleValidation}
Formik Schema Validation (e.g. yup)
Introduction
Define the field, the error conditions and the error.
const schema = Yup.object().shape({
name: Yup.string()
.min(2, 'Too Short!')
.max(50, 'Too Long!')
.required('Required'),
email: Yup.string()
.email('Invalid email')
.required('Required'),
})
Implementation in code
The password validation can be done with
import * as Yup from "yup";
...
const passwordSchema = Yup.object().shape({
password: Yup.string()
.required("Password cannot be empty")
.test("len", "Very Weak", (val) => val.length > 5)
.test("len", "Weak", (val) => val.length > 8),
});
Implementation in JSX
We can use the schema either directly from the Formik
<Formik
initialValues={{ email: "", password: "", rememberMe: false }}
onSumbit={this.handleSumbit}
validationSchema={PasswordSchema}
>
For on a field level
static validatePassword(value) {
let error;
try {
passwordSchema.validateSync({ password: value });
} catch (validationError) {
[error] = validationError.errors;
}
return error;
}
useField Hook
Input
useField hook accepts as input
- a field name string
- a props object A list of props you want to be able to set on the field. E.g. label.
// passing a name like email
const [field, meta, helpers] = useField('email');
// passing a props object
const MyTextField = ({ label, ...props }) => {
const [field, meta, helpers] = useField(props);
return (
<>
<label>
{label}
<input {...field} {...props} />
</label>
{meta.touched && meta.error ? (
<div className="error">{meta.error}</div>
) : null}
</>
);
};
Output
It returns an array of
- FieldInputProps
- FieldMetaProps
- FieldHelperProps
FieldInputProps
Key values are
- name
- value
- checked, multiple
- onBlur, onChange
FieldMetaProps
Contains computed meta data.
- error
- touched
- value
initialValue
FieldHelperProps
Contains methods for updating the value, touched or error status for the field. This can be done but using the
- setValue
- setTouched
- setError
These
Example 1
In this example we pass label as a prop to the useField.
/* eslint-disable react/jsx-props-no-spreading */
import React from "react";
import PropTypes from "prop-types";
import { useField } from "formik";
// Note passing the non-standard prop label separately.
const TestTextInput = ({ label, ...props }) => {
// Get the default field and meta props
const [field, meta] = useField(props);
// Add our label to the JSX
// Pass the field and props to the input
// Manage the display of the error
return (
<>
<label htmlFor={props.name}>{label}</label>
<input {...field} {...props} />
{meta.touched && meta.error ? <div>{meta.error}</div> : null}
</>
);
};
TestTextInput.propTypes = {
label: PropTypes.string,
name: PropTypes.string.isRequired,
};
TestTextInput.defaultProps = {
label: null,
};
export default TestTextInput;
Example 2
Here is a rating button example.
An below is the usage of this field.
Formik with Functional Components
Here is the sample from the Formik help showing how to make a similar form with the Formik hooks. The documentation is pretty good.
import React from 'react';
import { Formik } from 'formik';
import * as Yup from 'yup';
const SignupForm = () => {
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(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
}, 400);
}}
>
{formik => (
<form onSubmit={formik.handleSubmit}>
<label htmlFor="firstName">First Name</label>
<input
id="firstName"
type="text"
{...formik.getFieldProps('firstName')}
/>
{formik.touched.firstName && formik.errors.firstName ? (
<div>{formik.errors.firstName}</div>
) : null}
<label htmlFor="lastName">Last Name</label>
<input
id="lastName"
type="text"
{...formik.getFieldProps('lastName')}
/>
{formik.touched.lastName && formik.errors.lastName ? (
<div>{formik.errors.lastName}</div>
) : null}
<label htmlFor="email">Email Address</label>
<input id="email" type="email" {...formik.getFieldProps('email')} />
{formik.touched.email && formik.errors.email ? (
<div>{formik.errors.email}</div>
) : null}
<button type="submit">Submit</button>
</form>
)}
</Formik>
);
};
Usage of this would look like this where we are passing a label to always be associated with the field.
<TestTextInput
label="First Name"
name="firstname"
type="text"
placeholder="jane"
/>