Building Reusable and Dynamic Forms in React

Mohsin AliMohsin Ali | Jan 2025
Cover image for Building Reusable and Dynamic Forms in React

Introduction

Building dynamic and interactive forms in React can be challenging, especially when managing state and validation for complex forms. With React's powerful state management capabilities and hooks, creating forms becomes more efficient and streamlined. In this post, we’ll explore how to build a reusable form component with validation and state management using React.

Setting Up the Form Component

Let’s start by creating a reusable FormInput component to handle individual input fields.

import React from 'react';

const FormInput = ({ label, type, value, onChange, error }) => {
  return (
    <div>
      <label>{label}</label>
      <input 
        type={type} 
        value={value} 
        onChange={onChange} 
        style={{ borderColor: error ? 'red' : '#ccc' }} 
      />
      {error && <small style={{ color: 'red' }}>{error}</small>}
    </div>
  );
};

export default FormInput;

This component includes a label, an input field, and error handling for validation feedback. It accepts props like label, type, value, onChange, and error to make it reusable for various form fields.

Managing Form State and Validation

Next, we’ll create a parent form component to handle state and validation logic.

import React, { useState } from 'react';
import FormInput from './FormInput';

const Form = () => {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
  });

  const [errors, setErrors] = useState({});

  const validate = () => {
    const newErrors = {};
    if (!formData.username) newErrors.username = 'Username is required';
    if (!formData.email.includes('@')) newErrors.email = 'Email is invalid';
    if (formData.password.length < 6) newErrors.password = 'Password must be at least 6 characters';
    return newErrors;
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({ ...formData, [name]: value });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const validationErrors = validate();
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
    } else {
      console.log('Form submitted:', formData);
      setErrors({});
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <FormInput
        label="Username"
        type="text"
        value={formData.username}
        onChange={(e) => handleChange(e)}
        error={errors.username}
        name="username"
      />
      <FormInput
        label="Email"
        type="email"
        value={formData.email}
        onChange={(e) => handleChange(e)}
        error={errors.email}
        name="email"
      />
      <FormInput
        label="Password"
        type="password"
        value={formData.password}
        onChange={(e) => handleChange(e)}
        error={errors.password}
        name="password"
      />
      <button type="submit">Submit</button>
    </form>
  );
};

export default Form;

In this component, we manage the form state using useState and validate inputs with a custom validate function. Errors are displayed dynamically based on the validation results.

Styling the Form

Let’s add some basic styling to enhance the form's appearance.

form {
  max-width: 400px;
  margin: 0 auto;
  padding: 20px;
  border: 1px solid #ccc;
  border-radius: 8px;
  background-color: #f9f9f9;
}

div {
  margin-bottom: 15px;
}

label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

input {
  width: 100%;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 14px;
}

This styling gives the form a clean and professional look, making it more user-friendly.

Enhancing with Custom Hooks

For better scalability, you can use a custom hook to handle form state and validation logic.

import { useState } from 'react';

const useForm = (initialState, validate) => {
  const [formData, setFormData] = useState(initialState);
  const [errors, setErrors] = useState({});

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({ ...formData, [name]: value });
  };

  const handleSubmit = (callback) => (e) => {
    e.preventDefault();
    const validationErrors = validate(formData);
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
    } else {
      setErrors({});
      callback();
    }
  };

  return { formData, errors, handleChange, handleSubmit };
};

export default useForm;

That's how you have to setup useForm hook.

Conclusion

Creating reusable and dynamic forms in React not only simplifies your code but also improves maintainability. By leveraging components, hooks, and validation logic, you can build scalable forms for any application. Experiment with these techniques in your next React project and experience the benefits of cleaner and more efficient form management!