Paridhi SolutionsBlog
React

React Forms Explained - Complete Beginner Guide

Learn React Forms from scratch with practical examples. Understand controlled components, uncontrolled components, form validation, login forms, registration forms, and best practices.

Paridhi Solutions
2026-07-19
28 min read
React Forms Explained - Complete Beginner Guide

Introduction to React Forms

Forms are one of the most important parts of any web application. Whether you're building a login page, registration form, contact page, checkout process, search bar, or user profile, forms allow users to interact with your application by entering and submitting data.

In React, handling forms is slightly different from traditional HTML. Instead of letting the browser manage form data, React keeps form values inside the component's state. This gives developers complete control over user input and makes it easier to validate, update, and process data before sending it to a server.

If you've already learned React State (useState) and Event Handling, you're ready to understand React Forms.

In this guide, you'll learn everything about React forms from beginner to intermediate level using practical examples.


What You'll Learn

By the end of this tutorial, you'll be able to:

  • Create forms using React
  • Handle user input with useState
  • Understand controlled and uncontrolled components
  • Work with text inputs, textareas, dropdowns, radio buttons, and checkboxes
  • Submit forms correctly
  • Validate user input
  • Build login and registration forms
  • Display error messages
  • Reset form fields
  • Follow React best practices

Let's begin with the basics.


What is a Form?

A form is a collection of input fields that allows users to enter and submit information.

For example:

  • Login Form
  • Registration Form
  • Contact Form
  • Search Form
  • Feedback Form
  • Checkout Form

A simple HTML form looks like this:

html
<form>
    <input type="text" placeholder="Enter your name" />
    <button type="submit">Submit</button>
</form>

When the user clicks Submit, the browser sends the entered data to the server.

While this approach works well for traditional websites, React applications usually manage the data themselves before sending it to an API.


Why React Handles Forms Differently

React follows a concept called Single Source of Truth.

Instead of storing input values inside HTML elements, React stores them inside the component state.

This provides several benefits:

  • Live updates
  • Easy validation
  • Better user experience
  • Dynamic forms
  • Instant error messages
  • Easier API integration

Instead of asking the browser for the input value after submission, React always knows the current value.

For example:

Input Box
      │
      ▼
User Types
      │
      ▼
React State Updates
      │
      ▼
Component Re-renders
      │
      ▼
Updated UI

This is why React forms feel more interactive than traditional HTML forms.


HTML Forms vs React Forms

Traditional HTML Form

html
<form action="/login" method="POST">
    <input type="email" name="email" />
    <input type="password" name="password" />

    <button type="submit">
        Login
    </button>
</form>

Here the browser collects the values and submits the entire page.


React Form

jsx
function Login() {
    const [email, setEmail] = useState("");

    return (
        <input
            value={email}
            onChange={(e) => setEmail(e.target.value)}
        />
    );
}

React stores the value inside state instead of the browser.

This gives you full control over every keystroke.


Understanding Controlled Components

One of the most important concepts in React forms is the Controlled Component.

A controlled component is an input element whose value is controlled by React state.

For example:

jsx
import { useState } from "react";

function NameInput() {
    const [name, setName] = useState("");

    return (
        <>
            <input
                type="text"
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder="Enter your name"
            />

            <p>Your name is: {name}</p>
        </>
    );
}

export default NameInput;

In this example:

  • useState stores the input value.
  • value connects the input with state.
  • onChange updates the state whenever the user types.
  • React automatically updates the UI.

This is called a controlled component because React controls the input value.

Handling Different Types of Form Inputs

React allows you to work with every type of HTML form element while keeping their values synchronized with the component state.

Let's look at the most commonly used input fields.


Handling Text Input

Text inputs are the most common form elements.

Whenever a user types something, React updates the state using the onChange event.

jsx
import { useState } from "react";

function TextInputExample() {
    const [name, setName] = useState("");

    return (
        <div>
            <input
                type="text"
                placeholder="Enter your name"
                value={name}
                onChange={(e) => setName(e.target.value)}
            />

            <h3>Hello, {name || "Guest"}!</h3>
        </div>
    );
}

export default TextInputExample;

How it works

  1. The input starts with an empty string.
  2. The user types inside the input.
  3. onChange fires every time the value changes.
  4. setName() updates the state.
  5. React automatically re-renders the component.

This is the foundation of every React form.


Handling Multiple Text Inputs

Real applications rarely have just one input field.

For example, a registration form usually contains:

  • First Name
  • Last Name
  • Email
  • Phone Number

Each field can have its own state.

jsx
import { useState } from "react";

function UserForm() {
    const [firstName, setFirstName] = useState("");
    const [lastName, setLastName] = useState("");

    return (
        <>
            <input
                type="text"
                placeholder="First Name"
                value={firstName}
                onChange={(e) => setFirstName(e.target.value)}
            />

            <input
                type="text"
                placeholder="Last Name"
                value={lastName}
                onChange={(e) => setLastName(e.target.value)}
            />

            <h3>
                Welcome {firstName} {lastName}
            </h3>
        </>
    );
}

Although this works, managing many state variables becomes difficult for larger forms.

We'll solve that later using a single state object.


Handling Textarea

A textarea is used when users need to enter multiple lines of text.

Examples include:

  • Feedback
  • Comments
  • Messages
  • Product Reviews
jsx
import { useState } from "react";

function FeedbackForm() {
    const [message, setMessage] = useState("");

    return (
        <>
            <textarea
                rows="5"
                placeholder="Write your feedback..."
                value={message}
                onChange={(e) => setMessage(e.target.value)}
            />

            <p>{message}</p>
        </>
    );
}

Unlike HTML, React uses the value prop instead of placing text between the opening and closing <textarea> tags.


Character Counter Example

A common feature is displaying the number of characters entered.

jsx
import { useState } from "react";

function Bio() {
    const [bio, setBio] = useState("");

    return (
        <>
            <textarea
                value={bio}
                onChange={(e) => setBio(e.target.value)}
            />

            <p>{bio.length} / 300 characters</p>
        </>
    );
}

This provides instant feedback to users.


Handling Select Dropdown

Dropdowns allow users to choose one option from multiple values.

Example:

jsx
import { useState } from "react";

function CountrySelector() {
    const [country, setCountry] = useState("India");

    return (
        <>
            <select
                value={country}
                onChange={(e) => setCountry(e.target.value)}
            >
                <option>India</option>
                <option>United States</option>
                <option>Canada</option>
                <option>Australia</option>
            </select>

            <h3>Selected Country: {country}</h3>
        </>
    );
}

The selected option always stays synchronized with React state.


Rendering Dropdown Options Dynamically

Instead of writing each option manually, you can generate them using an array.

jsx
const countries = [
    "India",
    "USA",
    "Canada",
    "Germany",
    "Australia"
];

function CountryDropdown() {
    const [country, setCountry] = useState("");

    return (
        <select
            value={country}
            onChange={(e) => setCountry(e.target.value)}
        >
            <option value="">Select Country</option>

            {countries.map((item) => (
                <option key={item} value={item}>
                    {item}
                </option>
            ))}
        </select>
    );
}

Using arrays makes your code cleaner and easier to maintain.


Handling Radio Buttons

Radio buttons allow users to select only one option.

Example: Selecting Gender.

jsx
import { useState } from "react";

function GenderSelector() {
    const [gender, setGender] = useState("");

    return (
        <>
            <label>
                <input
                    type="radio"
                    value="Male"
                    checked={gender === "Male"}
                    onChange={(e) => setGender(e.target.value)}
                />
                Male
            </label>

            <label>
                <input
                    type="radio"
                    value="Female"
                    checked={gender === "Female"}
                    onChange={(e) => setGender(e.target.value)}
                />
                Female
            </label>

            <p>Selected: {gender}</p>
        </>
    );
}

Notice the use of the checked prop instead of value. This keeps the selected radio button in sync with the state.


Handling Checkboxes

Checkboxes are useful when users can select multiple options or toggle a setting.

Example: Accept Terms and Conditions.

jsx
import { useState } from "react";

function Terms() {
    const [accepted, setAccepted] = useState(false);

    return (
        <>
            <label>
                <input
                    type="checkbox"
                    checked={accepted}
                    onChange={(e) => setAccepted(e.target.checked)}
                />

                I accept the Terms and Conditions
            </label>

            <p>
                {accepted
                    ? "Thank you for accepting."
                    : "Please accept the terms."}
            </p>
        </>
    );
}

For checkboxes, use e.target.checked instead of e.target.value.


Selecting Multiple Interests

Users often need to choose more than one option.

Example:

jsx
const [skills, setSkills] = useState([]);

function handleSkillChange(event) {
    const value = event.target.value;

    if (event.target.checked) {
        setSkills([...skills, value]);
    } else {
        setSkills(skills.filter(skill => skill !== value));
    }
}

This approach allows users to select multiple values while React keeps everything synchronized.


Summary

In this section, you learned how to work with:

  • Text Inputs
  • Multiple Inputs
  • Textareas
  • Character Counters
  • Select Dropdowns
  • Dynamic Dropdown Options
  • Radio Buttons
  • Checkboxes
  • Multiple Checkbox Selection

These are the building blocks of almost every React form you'll create.

Managing Multiple Inputs with a Single State Object

In the previous examples, we created a separate state variable for each input field.

For small forms, this approach works well.

However, imagine a registration form with the following fields:

  • First Name
  • Last Name
  • Email
  • Phone Number
  • Address
  • City
  • Country
  • Password
  • Confirm Password

Creating a separate useState() hook for every field quickly becomes difficult to manage.

Instead, React developers usually store all form values inside a single object.


Creating a Form State Object

jsx
import { useState } from "react";

function RegisterForm() {
    const [formData, setFormData] = useState({
        firstName: "",
        lastName: "",
        email: "",
        phone: ""
    });

    return <div>Registration Form</div>;
}

Now every field belongs to one object.

formData
│
├── firstName
├── lastName
├── email
└── phone

This approach keeps your code organized and scalable.


Creating a Generic handleChange Function

Instead of writing four different event handlers, we can create one function that updates any field.

jsx
function handleChange(event) {
    const { name, value } = event.target;

    setFormData({
        ...formData,
        [name]: value
    });
}

Let's understand what's happening.

jsx
const { name, value } = event.target;

If the user types:

Name = email
Value = john@example.com

React receives:

javascript
name = "email"
value = "john@example.com"

Then this code updates only the matching property.

jsx
setFormData({
    ...formData,
    [name]: value
});

The spread operator (...formData) copies the existing values, while [name]: value updates only the changed field.


Complete Example

jsx
import { useState } from "react";

function RegisterForm() {

    const [formData, setFormData] = useState({
        firstName: "",
        lastName: "",
        email: ""
    });

    function handleChange(event) {

        const { name, value } = event.target;

        setFormData({
            ...formData,
            [name]: value
        });

    }

    return (

        <>

            <input
                name="firstName"
                placeholder="First Name"
                value={formData.firstName}
                onChange={handleChange}
            />

            <input
                name="lastName"
                placeholder="Last Name"
                value={formData.lastName}
                onChange={handleChange}
            />

            <input
                name="email"
                placeholder="Email"
                value={formData.email}
                onChange={handleChange}
            />

            <h3>{formData.firstName}</h3>
            <h3>{formData.lastName}</h3>
            <h3>{formData.email}</h3>

        </>

    );

}

export default RegisterForm;

Notice that every input shares the same handleChange() function.

This makes the code cleaner and much easier to maintain.


Understanding the name Attribute

The name attribute plays a very important role.

jsx
<input
    name="email"
    value={formData.email}
    onChange={handleChange}
/>

Inside the event handler:

javascript
event.target.name

returns

email

React then updates:

javascript
formData.email

automatically.

Without the name attribute, a generic event handler wouldn't know which field changed.


Handling Form Submission

After users fill in the form, the next step is submitting the data.

React uses the onSubmit event for this purpose.

jsx
function LoginForm() {

    function handleSubmit(event) {

        event.preventDefault();

        console.log("Form Submitted");

    }

    return (

        <form onSubmit={handleSubmit}>

            <button type="submit">
                Login
            </button>

        </form>

    );

}

Why Use preventDefault()?

Normally, submitting a form causes the browser to refresh the page.

React applications usually don't want that behavior.

That's why we call:

jsx
event.preventDefault();

This prevents the browser from reloading the page and allows React to handle the submission.

Without it:

Click Submit
        │
        ▼
Browser Refreshes
        │
        ▼
State Lost

With it:

Click Submit
        │
        ▼
React Handles Data
        │
        ▼
Call API
        │
        ▼
Show Success Message

This is how modern Single Page Applications (SPAs) work.


Building a Simple Login Form

Now let's combine everything we've learned.

jsx
import { useState } from "react";

function LoginForm() {

    const [formData, setFormData] = useState({
        email: "",
        password: ""
    });

    function handleChange(event) {

        const { name, value } = event.target;

        setFormData({
            ...formData,
            [name]: value
        });

    }

    function handleSubmit(event) {

        event.preventDefault();

        console.log(formData);

    }

    return (

        <form onSubmit={handleSubmit}>

            <input
                type="email"
                name="email"
                placeholder="Email"
                value={formData.email}
                onChange={handleChange}
            />

            <input
                type="password"
                name="password"
                placeholder="Password"
                value={formData.password}
                onChange={handleChange}
            />

            <button type="submit">
                Login
            </button>

        </form>

    );

}

export default LoginForm;

When the user clicks Login, the browser doesn't refresh. Instead, React prints the form data to the console.

{
    email: "john@example.com",
    password: "********"
}

In a real application, you would send this data to your backend API for authentication.


Best Practice

For forms with more than two or three fields, prefer storing the values in a single state object and updating them with a generic handleChange() function.

This approach reduces repetitive code, makes forms easier to maintain, and scales well as your application grows.

Building a Professional Login Form

In real-world applications, a login form is more than just two input fields. A good login experience provides:

  • Input validation
  • Password visibility toggle
  • Loading indicator
  • Error messages
  • Success messages
  • Secure form submission

Let's build a login form step by step.


Step 1: Creating the Form

jsx
import { useState } from "react";

function LoginForm() {

    const [formData, setFormData] = useState({
        email: "",
        password: ""
    });

    const [loading, setLoading] = useState(false);

    const [error, setError] = useState("");

    function handleChange(event) {

        const { name, value } = event.target;

        setFormData({
            ...formData,
            [name]: value
        });

    }

    return (

        <form>

            <input
                type="email"
                name="email"
                placeholder="Enter Email"
                value={formData.email}
                onChange={handleChange}
            />

            <input
                type="password"
                name="password"
                placeholder="Enter Password"
                value={formData.password}
                onChange={handleChange}
            />

            <button>
                Login
            </button>

        </form>

    );

}

export default LoginForm;

This creates a basic login form using a single state object.


Step 2: Validating User Input

Before submitting a form, always validate the input.

jsx
function handleSubmit(event) {

    event.preventDefault();

    if (!formData.email.trim()) {

        setError("Email is required.");

        return;

    }

    if (!formData.password.trim()) {

        setError("Password is required.");

        return;

    }

    setError("");

}

Now users cannot submit empty fields.


Displaying Validation Errors

jsx
{error &&

    <p className="error">
        {error}
    </p>

}

When validation fails, React automatically displays the error message.

Example:

Email is required.

Validating Email Format

Checking that the email follows a valid format helps catch common mistakes before the data reaches the server.

jsx
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

if (!emailPattern.test(formData.email)) {

    setError("Please enter a valid email address.");

    return;

}

Examples:

john@gmail.com      ✅ Valid
admin@yahoo.com     ✅ Valid
john@gmail          ❌ Invalid
abc                 ❌ Invalid

Always perform server-side validation as well. Client-side validation improves the user experience but should never be your only security layer.


Password Length Validation

Require a minimum password length to reduce weak passwords.

jsx
if (formData.password.length < 8) {

    setError("Password must be at least 8 characters.");

    return;

}

Example:

12345       ❌ Too Short
password    ✅ Valid

Adding a Loading State

In real applications, logging in usually involves an API request. During that request, users should know something is happening.

jsx
async function handleSubmit(event) {

    event.preventDefault();

    setLoading(true);

    try {

        await loginUser(formData);

    } finally {

        setLoading(false);

    }

}

Update the button to reflect the loading state:

jsx
<button
    disabled={loading}
>

    {loading ? "Logging in..." : "Login"}

</button>

Now users can't accidentally submit the form multiple times.


Adding a Password Visibility Toggle

Password fields often include a "Show Password" option.

jsx
const [showPassword, setShowPassword] = useState(false);

Update the password input:

jsx
<input
    type={showPassword ? "text" : "password"}
    name="password"
    value={formData.password}
    onChange={handleChange}
/>

Add a toggle button:

jsx
<button
    type="button"
    onClick={() => setShowPassword(!showPassword)}
>

    {showPassword ? "Hide" : "Show"}

</button>

This small feature improves usability, especially on mobile devices.


Complete Login Form Example

jsx
import { useState } from "react";

export default function LoginForm() {

    const [formData, setFormData] = useState({
        email: "",
        password: ""
    });

    const [loading, setLoading] = useState(false);
    const [showPassword, setShowPassword] = useState(false);
    const [error, setError] = useState("");

    function handleChange(event) {

        const { name, value } = event.target;

        setFormData({
            ...formData,
            [name]: value
        });

    }

    async function handleSubmit(event) {

        event.preventDefault();

        if (!formData.email || !formData.password) {

            setError("Please fill in all fields.");

            return;

        }

        setError("");

        setLoading(true);

        try {

            console.log(formData);

            // Call your login API here.

        } catch (err) {

            setError("Login failed. Please try again.");

        } finally {

            setLoading(false);

        }

    }

    return (

        <form onSubmit={handleSubmit}>

            {error && <p>{error}</p>}

            <input
                type="email"
                name="email"
                placeholder="Email"
                value={formData.email}
                onChange={handleChange}
            />

            <input
                type={showPassword ? "text" : "password"}
                name="password"
                placeholder="Password"
                value={formData.password}
                onChange={handleChange}
            />

            <button
                type="button"
                onClick={() => setShowPassword(!showPassword)}
            >
                {showPassword ? "Hide Password" : "Show Password"}
            </button>

            <button
                type="submit"
                disabled={loading}
            >
                {loading ? "Logging in..." : "Login"}
            </button>

        </form>

    );

}

User Experience Tips

A professional login form should:

  • Validate fields before submission.
  • Clearly explain what went wrong.
  • Prevent duplicate submissions.
  • Indicate when a request is in progress.
  • Allow users to view or hide their password.
  • Keep error messages simple and actionable.
  • Never expose sensitive information such as passwords in logs or UI messages.

These small improvements make your forms feel polished and significantly improve the overall user experience.

Building a Complete Registration Form

Registration forms are more complex than login forms because they collect additional information and require more validation.

A typical registration form includes:

  • Full Name
  • Email Address
  • Phone Number
  • Password
  • Confirm Password
  • Terms & Conditions

Let's build one step by step.


Creating the Form State

Instead of using multiple useState() hooks, we'll continue using a single state object.

jsx
const [formData, setFormData] = useState({
    fullName: "",
    email: "",
    phone: "",
    password: "",
    confirmPassword: "",
    acceptTerms: false
});

This keeps all form values organized in one place.


Generic Input Handler

Our existing handleChange() function works for text fields, but checkboxes require a small modification.

jsx
function handleChange(event) {

    const { name, value, type, checked } = event.target;

    setFormData({
        ...formData,
        [name]: type === "checkbox"
            ? checked
            : value
    });

}

Now the same function can update both text inputs and checkboxes.


Building the Registration Form

jsx
<form onSubmit={handleSubmit}>

    <input
        type="text"
        name="fullName"
        placeholder="Full Name"
        value={formData.fullName}
        onChange={handleChange}
    />

    <input
        type="email"
        name="email"
        placeholder="Email Address"
        value={formData.email}
        onChange={handleChange}
    />

    <input
        type="tel"
        name="phone"
        placeholder="Phone Number"
        value={formData.phone}
        onChange={handleChange}
    />

    <input
        type="password"
        name="password"
        placeholder="Password"
        value={formData.password}
        onChange={handleChange}
    />

    <input
        type="password"
        name="confirmPassword"
        placeholder="Confirm Password"
        value={formData.confirmPassword}
        onChange={handleChange}
    />

</form>

At this stage, users can enter their information, but we still need to validate it before submission.


Validating Required Fields

Before submitting the form, check that every required field contains a value.

jsx
if (
    !formData.fullName ||
    !formData.email ||
    !formData.phone ||
    !formData.password ||
    !formData.confirmPassword
) {

    setError("Please fill in all required fields.");

    return;

}

This prevents incomplete registrations.


Confirm Password Validation

One of the most common validation checks is ensuring that both password fields match.

jsx
if (
    formData.password !==
    formData.confirmPassword
) {

    setError("Passwords do not match.");

    return;

}

Example:

Password

React123

Confirm Password

React1234

❌ Passwords do not match.

Password Strength Validation

A strong password improves account security.

Example validation:

jsx
if (formData.password.length < 8) {

    setError(
        "Password must contain at least 8 characters."
    );

    return;

}

You can make this even stronger by checking for:

  • Uppercase letters
  • Lowercase letters
  • Numbers
  • Special characters

Example:

password

❌ Weak

React123

⚠ Medium

React@123

✅ Strong

Accepting Terms & Conditions

Most applications require users to accept their Terms of Service.

jsx
<label>

    <input
        type="checkbox"
        name="acceptTerms"
        checked={formData.acceptTerms}
        onChange={handleChange}
    />

    I agree to the Terms & Conditions

</label>

Validation:

jsx
if (!formData.acceptTerms) {

    setError(
        "Please accept the Terms & Conditions."
    );

    return;

}

Resetting the Form After Success

After a successful registration, it's common to clear the form.

jsx
setFormData({

    fullName: "",
    email: "",
    phone: "",
    password: "",
    confirmPassword: "",
    acceptTerms: false

});

The form immediately returns to its initial state.


Complete Registration Form Example

jsx
import { useState } from "react";

export default function RegisterForm() {

    const [formData, setFormData] = useState({

        fullName: "",
        email: "",
        phone: "",
        password: "",
        confirmPassword: "",
        acceptTerms: false

    });

    const [error, setError] = useState("");

    function handleChange(event) {

        const { name, value, type, checked } = event.target;

        setFormData({

            ...formData,

            [name]:
                type === "checkbox"
                    ? checked
                    : value

        });

    }

    function handleSubmit(event) {

        event.preventDefault();

        if (
            formData.password !==
            formData.confirmPassword
        ) {

            setError("Passwords do not match.");

            return;

        }

        if (!formData.acceptTerms) {

            setError("Please accept the Terms.");

            return;

        }

        setError("");

        console.log(formData);

    }

    return (

        <form onSubmit={handleSubmit}>

            {error && <p>{error}</p>}

            <input
                name="fullName"
                placeholder="Full Name"
                value={formData.fullName}
                onChange={handleChange}
            />

            <input
                name="email"
                placeholder="Email"
                value={formData.email}
                onChange={handleChange}
            />

            <input
                type="password"
                name="password"
                placeholder="Password"
                value={formData.password}
                onChange={handleChange}
            />

            <input
                type="password"
                name="confirmPassword"
                placeholder="Confirm Password"
                value={formData.confirmPassword}
                onChange={handleChange}
            />

            <label>

                <input
                    type="checkbox"
                    name="acceptTerms"
                    checked={formData.acceptTerms}
                    onChange={handleChange}
                />

                I agree to the Terms & Conditions

            </label>

            <button type="submit">
                Register
            </button>

        </form>

    );

}

Best Practices for Registration Forms

Follow these recommendations when building registration forms:

  • Keep the form as short as possible.
  • Validate data before sending it to the server.
  • Display clear and helpful error messages.
  • Never store plain-text passwords.
  • Hash passwords on the server before saving them.
  • Use HTTPS when transmitting sensitive information.
  • Disable the submit button while the request is processing.
  • Consider adding email verification for new accounts.
  • Provide meaningful placeholders and labels for accessibility.

Summary

You now know how to build a complete registration form using React. By combining controlled components, a single state object, generic input handling, and client-side validation, you can create forms that are easy to maintain and provide a better user experience.

In the next section, we'll build a Contact Form and explore techniques like form submission, success messages, loading indicators, and integration with backend APIs.

Building a Contact Form

A contact form is one of the most common features you'll build in a React application. Businesses, portfolios, blogs, and e-commerce websites all use contact forms to collect inquiries from users.

A typical contact form contains:

  • Name
  • Email Address
  • Subject
  • Message
  • Submit Button

Unlike login or registration forms, contact forms usually send the data to a backend API or an email service.


Creating the Form State

We'll use a single state object to store all field values.

jsx
import { useState } from "react";

function ContactForm() {

    const [formData, setFormData] = useState({
        name: "",
        email: "",
        subject: "",
        message: ""
    });

    return <div>Contact Form</div>;
}

Generic Change Handler

Since every field belongs to the same object, one event handler is enough.

jsx
function handleChange(event) {

    const { name, value } = event.target;

    setFormData({
        ...formData,
        [name]: value
    });

}

Now every input simply uses:

jsx
onChange={handleChange}

Creating the Contact Form

jsx
<form onSubmit={handleSubmit}>

    <input
        type="text"
        name="name"
        placeholder="Your Name"
        value={formData.name}
        onChange={handleChange}
    />

    <input
        type="email"
        name="email"
        placeholder="Your Email"
        value={formData.email}
        onChange={handleChange}
    />

    <input
        type="text"
        name="subject"
        placeholder="Subject"
        value={formData.subject}
        onChange={handleChange}
    />

    <textarea
        name="message"
        rows="6"
        placeholder="Write your message..."
        value={formData.message}
        onChange={handleChange}
    />

    <button type="submit">
        Send Message
    </button>

</form>

Validating User Input

Before sending the form, ensure every required field is filled.

jsx
if (
    !formData.name ||
    !formData.email ||
    !formData.subject ||
    !formData.message
) {

    setError("Please fill in all fields.");

    return;

}

You can also validate the email format.

jsx
const emailRegex =
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

if (!emailRegex.test(formData.email)) {

    setError("Please enter a valid email.");

    return;

}

Sending Form Data

In most applications, you'll send the form data to an API.

jsx
async function handleSubmit(event) {

    event.preventDefault();

    try {

        await fetch("/api/contact", {

            method: "POST",

            headers: {
                "Content-Type": "application/json"
            },

            body: JSON.stringify(formData)

        });

    } catch (error) {

        console.error(error);

    }

}

Your backend can then:

  • Save the inquiry
  • Send an email
  • Create a support ticket
  • Store the data in a database

Showing a Loading Indicator

API requests take time.

Always let users know that something is happening.

jsx
const [loading, setLoading] =
    useState(false);

Inside the submit function:

jsx
setLoading(true);

try {

    await fetch(...);

} finally {

    setLoading(false);

}

Button:

jsx
<button
    disabled={loading}
>

    {loading
        ? "Sending..."
        : "Send Message"}

</button>

This prevents duplicate submissions.


Displaying Success Messages

After a successful submission, show a confirmation message.

jsx
const [success, setSuccess] =
    useState(false);

After the API call:

jsx
setSuccess(true);

Display it:

jsx
{success && (

<p>

    Thank you!

    We received your message.

</p>

)}

This reassures users that their request has been received.


Resetting the Form

After submitting successfully, clear the inputs.

jsx
setFormData({

    name: "",
    email: "",
    subject: "",
    message: ""

});

This prepares the form for another submission.


Complete Contact Form Example

jsx
import { useState } from "react";

export default function ContactForm() {

    const [formData, setFormData] = useState({

        name: "",
        email: "",
        subject: "",
        message: ""

    });

    const [loading, setLoading] = useState(false);
    const [success, setSuccess] = useState(false);
    const [error, setError] = useState("");

    function handleChange(event) {

        const { name, value } = event.target;

        setFormData({

            ...formData,

            [name]: value

        });

    }

    async function handleSubmit(event) {

        event.preventDefault();

        if (
            !formData.name ||
            !formData.email ||
            !formData.subject ||
            !formData.message
        ) {

            setError("Please fill in all fields.");

            return;

        }

        setError("");

        setLoading(true);

        try {

            console.log(formData);

            // Replace with your actual API call.
            // await fetch("/api/contact", ...);

            setSuccess(true);

            setFormData({

                name: "",
                email: "",
                subject: "",
                message: ""

            });

        } catch (err) {

            setError("Something went wrong.");

        } finally {

            setLoading(false);

        }

    }

    return (

        <form onSubmit={handleSubmit}>

            {error && <p>{error}</p>}

            {success && (
                <p>
                    Thank you! Your message has been sent.
                </p>
            )}

            <input
                name="name"
                placeholder="Your Name"
                value={formData.name}
                onChange={handleChange}
            />

            <input
                type="email"
                name="email"
                placeholder="Email"
                value={formData.email}
                onChange={handleChange}
            />

            <input
                name="subject"
                placeholder="Subject"
                value={formData.subject}
                onChange={handleChange}
            />

            <textarea
                name="message"
                rows="5"
                placeholder="Message"
                value={formData.message}
                onChange={handleChange}
            />

            <button
                type="submit"
                disabled={loading}
            >
                {loading
                    ? "Sending..."
                    : "Send Message"}
            </button>

        </form>

    );

}

Common Mistakes When Working with Forms

Even experienced developers occasionally make mistakes when building forms. Here are some of the most common issues to avoid.

Forgetting preventDefault()

If you don't call event.preventDefault(), the browser refreshes the page after form submission.

jsx
function handleSubmit(event) {

    event.preventDefault();

}

Mutating State Directly

Never modify the state object directly.

❌ Incorrect

jsx
formData.name = "John";

✅ Correct

jsx
setFormData({
    ...formData,
    name: "John"
});

Forgetting the name Attribute

Your generic handleChange() depends on the name attribute.

jsx
<input onChange={handleChange} />

jsx
<input
    name="email"
    onChange={handleChange}
/>

Not Handling API Errors

Network requests can fail. Always handle errors gracefully.

jsx
try {

    await fetch(...);

} catch (error) {

    setError(
        "Unable to send your message."
    );

}

Summary

You now know how to build a complete contact form using React. Along the way, you've learned how to manage form state, validate user input, submit data, display loading and success messages, reset the form, and avoid common mistakes.

These techniques apply not only to contact forms but also to login forms, registration forms, checkout pages, feedback forms, and many other real-world applications.

React Forms Best Practices

As your React applications grow, forms become more complex. Following best practices helps you build forms that are easier to maintain, more user-friendly, and less prone to bugs.

Here are some recommendations followed by experienced React developers.


1. Use Controlled Components

Whenever possible, let React manage the form state.

✅ Good

jsx
<input
    value={name}
    onChange={(e) => setName(e.target.value)}
/>

Avoid relying on the DOM to read input values after submission.

Keeping React in control makes validation, conditional rendering, and API integration much simpler.


Instead of creating many state variables:

jsx
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");

Use a single object.

jsx
const [formData, setFormData] = useState({
    firstName: "",
    lastName: "",
    email: ""
});

This approach scales much better for large forms.


3. Reuse a Generic Change Handler

Instead of writing multiple functions,

jsx
handleFirstName()
handleLastName()
handleEmail()

Create one reusable function.

jsx
function handleChange(event) {

    const { name, value } = event.target;

    setFormData({
        ...formData,
        [name]: value
    });

}

This keeps your code clean and reduces duplication.


4. Validate Before Sending Data

Never send invalid data to your backend.

Check for:

  • Required fields
  • Email format
  • Password strength
  • Password confirmation
  • Phone number format
  • Accepted terms

Client-side validation improves the user experience, but always validate again on the server for security.


5. Display Helpful Error Messages

Avoid generic messages like:

Error

Instead, explain what went wrong.

Examples:

✅ Please enter your email address.

✅ Password must contain at least 8 characters.

✅ Confirm Password does not match.

Helpful messages reduce user frustration.


6. Disable the Submit Button During Requests

Users may click the submit button multiple times.

Prevent duplicate requests by disabling the button while processing.

jsx
<button
    disabled={loading}
>

    {loading
        ? "Submitting..."
        : "Submit"}

</button>

This improves both user experience and backend reliability.


7. Reset the Form Only After Success

Don't clear the form before confirming the request succeeded.

jsx
setFormData({
    name: "",
    email: "",
    message: ""
});

If an error occurs, users won't lose the information they already entered.


8. Keep Components Small

Large forms can become difficult to maintain.

Instead of creating one huge component:

RegistrationForm.jsx

Split it into smaller components.

PersonalInfo.jsx

AddressForm.jsx

PasswordSection.jsx

TermsCheckbox.jsx

Smaller components are easier to test and reuse.


Accessibility Tips

Accessibility ensures your forms can be used by everyone, including people who rely on assistive technologies.


Always Use Labels

❌ Bad

jsx
<input placeholder="Email" />

✅ Better

jsx
<label htmlFor="email">

    Email Address

</label>

<input
    id="email"
    type="email"
/>

Labels improve accessibility and make forms easier to use.


Use Meaningful Placeholder Text

Instead of:

Enter...

Use:

Enter your email address

Clear placeholders guide users more effectively.


Show Validation Messages Clearly

If validation fails, display the message near the relevant field.

Example:

Email Address

[____________]

Please enter a valid email address.

Users immediately know what needs to be fixed.


Support Keyboard Navigation

Users should be able to navigate your form using:

  • Tab
  • Shift + Tab
  • Enter
  • Space

Avoid creating interfaces that only work with a mouse.


Use Appropriate Input Types

HTML provides specialized input types.

jsx
type="email"

type="password"

type="tel"

type="number"

type="date"

These improve both accessibility and the mobile user experience by displaying the most appropriate keyboard.


Frequently Asked Questions (FAQs)

What is a controlled component?

A controlled component is an input element whose value is managed by React state.


Why do we use useState with forms?

useState stores the current value of each input so React can update the UI whenever the user types.


What is onChange?

The onChange event runs every time the value of an input changes. It allows React to keep the component state synchronized with user input.


Why is event.preventDefault() important?

By default, browsers refresh the page when a form is submitted.

Calling event.preventDefault() stops that behavior and lets React handle the submission without reloading the page.


Should I validate forms in React or on the server?

Both.

  • Client-side validation provides instant feedback.
  • Server-side validation protects your application from invalid or malicious data.

Never rely solely on client-side validation.


When should I use multiple useState() hooks?

For very small forms with one or two fields.

For larger forms, storing related values in a single state object is generally easier to maintain.


React Forms Interview Questions

1. What is a controlled component?


2. What is an uncontrolled component?


3. Why do we use useState() in forms?


4. What is the purpose of onChange?


5. Why should we call event.preventDefault()?


6. How do you manage multiple form fields with one event handler?


7. How do you validate forms in React?


8. How do you reset a form?


9. What's the difference between value and defaultValue?


10. What are some common mistakes when building React forms?

These questions are frequently asked in React developer interviews, especially for junior and mid-level roles.


Summary

Congratulations! 🎉

You have learned how to build forms in React from the ground up.

In this guide, you covered:

  • Understanding React forms
  • Controlled components
  • Handling text inputs
  • Textareas
  • Select dropdowns
  • Radio buttons
  • Checkboxes
  • Managing multiple fields with one state object
  • Generic handleChange()
  • Form submission
  • Login form
  • Registration form
  • Contact form
  • Client-side validation
  • Loading states
  • Success messages
  • Resetting forms
  • Accessibility
  • Best practices
  • Common mistakes
  • Interview questions

These concepts form the foundation of nearly every React application, whether you're building a simple contact page or a full-scale e-commerce platform.


What's Next?

Now that you know how to collect user input, it's time to learn how React interacts with the outside world.

In the next tutorial, we'll explore the React useEffect Hook, where you'll learn how to:

  • Run code after rendering
  • Fetch data from APIs
  • Use dependency arrays
  • Clean up side effects
  • Work with timers and event listeners
  • Save data to Local Storage
  • Avoid infinite loops
  • Follow React best practices

By the end of the next guide, you'll understand one of the most powerful and commonly used hooks in React.

Happy Coding! 🚀

Tags

ReactFormsuseStateValidationReact Tutorial

Written by

Paridhi Solutions

Publishing in-depth tutorials, coding guides, and best practices for modern web development.

Visit Website →

Share this article

Related Articles