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