Frontend Development
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.
Technologies
Key points
- 01
How to structure a basic controlled input component in React
- 02
Managing form state dynamically using a single useState object
- 03
Implementing basic validation logic for required fields
- 04
Making the form completely reusable with a configuration-based approach
- 05
How to scale and extend the form with new field types and validation rules
Example 01
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>
);Example 02
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>
);
};Example 03
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} />;
}References
- Forms in React
- Type Safety
- Reusable Components
- Frontend Architecture
- Component Design