Creating a Reusable Form Component in React with TypeScript (No Third-Party Libraries)
Learn how to build a fully reusable and type-safe form component in React using TypeScript without relying on third-party libraries. This guide focuses on simplicity, scalability, and clarity for professional front-end engineers.
Summary
In this article, we build a modular and reusable form component using React and TypeScript from scratch. You’ll learn how to manage state dynamically, implement basic validation, and reuse the form logic across different components—all without using any third-party libraries like react-hook-form or Formik.
Key Points
How to structure a basic controlled input component in React
Managing form state dynamically using a single useState object
Implementing basic validation logic for required fields
Making the form completely reusable with a configuration-based approach
How to scale and extend the form with new field types and validation rules
Code Examples
1. 1. Basic Form Input Component
interface FormInputProps {
label: string;
name: string;
type?: string;
value: string;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
error?: string;
}
export const FormInput = ({ label, name, type = "text", value, onChange, error }: FormInputProps) => (
<div className="mb-4">
<label htmlFor={name} className="block font-medium">{label}</label>
<input
id={name}
name={name}
type={type}
value={value}
onChange={onChange}
className={`w-full border p-2 rounded ${error ? 'border-red-500' : 'border-gray-300'}`}
/>
{error && <p className="text-red-500 text-sm">{error}</p>}
</div>
);2. 2. Reusable Form Component
type FieldType = {
name: string;
label: string;
type?: string;
required?: boolean;
};
interface ReusableFormProps {
fields: FieldType[];
onSubmit: (formData: Record<string, string>) => void;
}
export const ReusableForm = ({ fields, onSubmit }: ReusableFormProps) => {
const [formData, setFormData] = useState(
fields.reduce((acc, field) => ({ ...acc, [field.name]: "" }), {})
);
const [errors, setErrors] = useState<Record<string, string>>({});
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
setErrors(prev => ({ ...prev, [name]: "" }));
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
const newErrors: Record<string, string> = {};
fields.forEach(field => {
if (field.required && !formData[field.name].trim()) {
newErrors[field.name] = `${field.label} is required.`;
}
});
if (Object.keys(newErrors).length) {
setErrors(newErrors);
return;
}
onSubmit(formData);
};
return (
<form onSubmit={handleSubmit}>
{fields.map(field => (
<FormInput
key={field.name}
label={field.label}
name={field.name}
type={field.type}
value={formData[field.name]}
onChange={handleChange}
error={errors[field.name]}
/>
))}
<button type="submit" className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">Submit</button>
</form>
);
};3. 3. Usage Example
const fields = [
{ name: "name", label: "Name", required: true },
{ name: "email", label: "Email", type: "email", required: true },
{ name: "phone", label: "Phone" },
];
export default function FormPage() {
const handleSubmit = (data: Record<string, string>) => {
console.log("Form Submitted", data);
};
return <ReusableForm fields={fields} onSubmit={handleSubmit} />;
}