Building Type-Safe Forms with Formik and Yup in React TypeScript
Erik Nguyen / January 6, 2025
Building Type-Safe Forms with Formik and Yup in React TypeScript
Form handling in React can be challenging, especially when you need to manage complex validation rules and ensure type safety. This guide will show you how to leverage Formik and Yup to create robust, type-safe forms in your React TypeScript applications.
Understanding the Stack
Before we dive in, let's understand why we're using these specific tools:
- Formik: Handles form state management, validation, and submission
- Yup: Provides a schema-based approach to validation with TypeScript support
- TypeScript: Adds static typing to ensure type safety throughout our application
Setting Up Your Project
First, let's install the necessary dependencies:
npm install formik yup @types/yup
Creating a Type-Safe Schema
Let's start by defining our form schema using Yup:
import * as Yup from 'yup';
// Define the validation schema
const userFormSchema = Yup.object().shape({
username: Yup.string()
.min(3, 'Username must be at least 3 characters')
.required('Username is required'),
email: Yup.string()
.email('Invalid email address')
.required('Email is required'),
password: Yup.string()
.min(8, 'Password must be at least 8 characters')
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
'Password must contain at least one uppercase letter, one lowercase letter, and one number'
)
.required('Password is required'),
});
// Infer the TypeScript type from the schema
type UserFormValues = Yup.InferType<typeof userFormSchema>;
Implementing the Form Component
Now, let's create a form component that uses Formik with our schema:
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
const UserRegistrationForm: React.FC = () => {
// Initial form values matching our schema
const initialValues: UserFormValues = {
username: '',
email: '',
password: '',
};
const handleSubmit = async (values: UserFormValues) => {
try {
// Here you would typically make an API call
console.log('Form submitted:', values);
} catch (error) {
console.error('Submission error:', error);
}
};
return (
<Formik
initialValues={initialValues}
validationSchema={userFormSchema}
onSubmit={handleSubmit}
>
{({ isSubmitting, touched, errors }) => (
<Form className="space-y-4">
<div>
<label htmlFor="username" className="block mb-2">
Username
</label>
<Field
type="text"
id="username"
name="username"
className={`w-full p-2 border rounded ${
touched.username && errors.username ? 'border-red-500' : 'border-gray-300'
}`}
/>
<ErrorMessage
name="username"
component="div"
className="text-red-500 text-sm mt-1"
/>
</div>
<div>
<label htmlFor="email" className="block mb-2">
Email
</label>
<Field
type="email"
id="email"
name="email"
className={`w-full p-2 border rounded ${
touched.email && errors.email ? 'border-red-500' : 'border-gray-300'
}`}
/>
<ErrorMessage
name="email"
component="div"
className="text-red-500 text-sm mt-1"
/>
</div>
<div>
<label htmlFor="password" className="block mb-2">
Password
</label>
<Field
type="password"
id="password"
name="password"
className={`w-full p-2 border rounded ${
touched.password && errors.password ? 'border-red-500' : 'border-gray-300'
}`}
/>
<ErrorMessage
name="password"
component="div"
className="text-red-500 text-sm mt-1"
/>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600 disabled:bg-blue-300"
>
{isSubmitting ? 'Submitting...' : 'Register'}
</button>
</Form>
)}
</Formik>
);
};
export default UserRegistrationForm;
Adding Custom Field Components
To make our form more reusable, let's create a custom field component:
interface FormFieldProps {
label: string;
name: string;
type?: string;
placeholder?: string;
}
const FormField: React.FC<FormFieldProps> = ({ label, name, type = 'text', placeholder }) => {
return (
<div>
<label htmlFor={name} className="block mb-2">
{label}
</label>
<Field
type={type}
id={name}
name={name}
placeholder={placeholder}
className="w-full p-2 border rounded"
/>
<ErrorMessage
name={name}
component="div"
className="text-red-500 text-sm mt-1"
/>
</div>
);
};
Handling Form Submission
Here's how to properly handle form submission with TypeScript:
const handleSubmit = async (
values: UserFormValues,
{ setSubmitting, setErrors }: FormikHelpers<UserFormValues>
) => {
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// Handle successful submission
console.log('Form submitted successfully:', values);
// You could redirect here
// navigate('/success');
} catch (error) {
// Handle submission error
setErrors({
submit: 'An error occurred while submitting the form'
});
} finally {
setSubmitting(false);
}
};
Best Practices and Tips
- Type Safety: Always use TypeScript interfaces or types for your form values:
interface UserForm {
username: string;
email: string;
password: string;
}
- Custom Validation: You can combine Yup with custom validation functions:
const validateUsername = async (value: string) => {
// Simulate API call to check username availability
const isAvailable = await checkUsernameAvailability(value);
return isAvailable ? undefined : 'Username is already taken';
};
- Form State Management: Use Formik's built-in hooks for accessing form state:
import { useFormikContext } from 'formik';
const FormStatus: React.FC = () => {
const { isSubmitting, isValid } = useFormikContext();
return (
<div>
{isSubmitting && <p>Submitting...</p>}
{!isValid && <p>Please fix form errors</p>}
</div>
);
};
Conclusion
By combining Formik with Yup in a TypeScript environment, we've created a type-safe, validated form system that provides excellent developer experience and runtime safety. This approach scales well for both simple and complex forms while maintaining code quality and type safety throughout your application.
Remember to:
- Always define your validation schema first
- Use TypeScript types derived from your Yup schema
- Leverage Formik's built-in components and hooks
- Create reusable components for common form patterns
Happy coding!