Frontend Development
12 min
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.
Technologies Used
React 18.xTypeScript 5.x
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. 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. 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. 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
Related Topics
Forms in ReactType SafetyReusable ComponentsFrontend ArchitectureComponent Design