React Tutorial 0/120 lessons ~6 min read Lesson 25

    Forms in React

    Forms in React: controlled inputs, form state objects, validation, checkboxes, radios, selects, submission, loading, and controlled vs uncontrolled patterns.

    Course progress0%
    Focus
    44 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Forms in React

    Forms in React are one of the foundational pieces of modern React engineering.

    Forms are everywhere in real-world applications:

    • Login and registration
    • Search
    • Contact forms
    • Checkout
    • User profiles
    • Feedback and surveys
    • Settings and authentication
    • Data entry, filtering, and configuration

    Unlike traditional HTML forms, React applications commonly use state to control form values and event handlers to manage user input and submission.

    Learning Objectives

    By the end of this lesson, you will understand:

    1. Controlled components and why they matter
    2. Managing one field vs one form state object
    3. onSubmit, preventDefault(), and submission flows
    4. Text, number, checkbox, radio, select, and textarea controls
    5. Validation, errors, loading, and reset patterns
    6. Controlled vs uncontrolled inputs and refs
    7. Common mistakes, best practices, and interview questions

    1. What Is a Form?

    A form is a collection of controls that allows users to enter or select information.

    tsx
    <form>
    <input type="text" />
    <input type="email" />
    <button type="submit">
    Submit
    </button>
    </form>

    In React, we typically connect these controls to component state:

    text
    User enters data
    ↓
    Input event
    ↓
    Event handler
    ↓
    State update
    ↓
    React re-renders
    ↓
    UI reflects current value

    2. A Basic React Form

    tsx
    function LoginForm() {
    return (
    <form>
    <input type="email" />
    <input type="password" />
    <button type="submit">
    Login
    </button>
    </form>
    );
    }

    This creates the form UI, but React is not yet managing the values. Introduce state to make the form interactive.

    3. Controlled Components

    The most important concept in React forms is the controlled component. A controlled input gets its value from React state.

    tsx
    import { useState } from "react";
    function LoginForm() {
    const [email, setEmail] = useState("");
    return (
    <input
    type="email"
    value={email}
    onChange={(event) => {
    setEmail(event.target.value);
    }}
    />
    );
    }

    value={email} means React controls the input's current value. onChange updates state when the user types.

    text
    User types
    ↓
    onChange
    ↓
    event.target.value
    ↓
    setEmail()
    ↓
    email state changes
    ↓
    React renders
    ↓
    input displays new value

    4. Why Controlled Components Matter

    Controlled inputs give React a single source of truth.

    text
    Without React state:
    Input → Browser owns value
    With a controlled component:
    React State → Input value ← onChange

    This makes it easier to validate, disable buttons, show live feedback, reset forms, submit data, transform values, and display errors.

    5. Handling Text Inputs

    tsx
    import { useState } from "react";
    function ProfileForm() {
    const [name, setName] = useState("");
    function handleNameChange(event) {
    setName(event.target.value);
    }
    return (
    <input
    type="text"
    value={name}
    onChange={handleNameChange}
    />
    );
    }

    The important pieces are event.target.value and value={name}.

    6. Handling Multiple Inputs

    One approach is separate state variables:

    tsx
    const [name, setName] = useState("");
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");
    <input
    value={name}
    onChange={(event) => setName(event.target.value)}
    />
    <input
    value={email}
    onChange={(event) => setEmail(event.target.value)}
    />
    <input
    value={password}
    onChange={(event) => setPassword(event.target.value)}
    />

    This is valid for smaller forms.

    7. Managing Form Data with One State Object

    For larger forms, keep related fields together:

    tsx
    const [form, setForm] = useState({
    name: "",
    email: "",
    password: ""
    });
    function handleChange(event) {
    const { name, value } = event.target;
    setForm({
    ...form,
    [name]: value
    });
    }
    <input
    name="name"
    value={form.name}
    onChange={handleChange}
    />
    <input
    name="email"
    value={form.email}
    onChange={handleChange}
    />
    <input
    name="password"
    value={form.password}
    onChange={handleChange}
    />
    text
    name="email"
    ↓
    event.target.name → "email"
    event.target.value → new value
    ↓
    setForm()

    8. Updating Objects in State

    Preserve other fields with the spread operator:

    tsx
    setForm({
    ...form,
    [name]: value
    });
    // User changes email:
    setForm({
    ...form,
    email: "new@example.com"
    });
    // → { name: "Rahul", email: "new@example.com", password: "123456" }

    9. Prefer Functional State Updates

    When the next state depends on the previous state, a functional update is clearer and safer:

    tsx
    setForm((currentForm) => ({
    ...currentForm,
    [name]: value
    }));

    10. Form Submission

    tsx
    function LoginForm() {
    function handleSubmit(event) {
    event.preventDefault();
    console.log("Form submitted");
    }
    return (
    <form onSubmit={handleSubmit}>
    <input type="email" />
    <input type="password" />
    <button type="submit">
    Login
    </button>
    </form>
    );
    }

    11. Why Use preventDefault()?

    Browsers may navigate or reload on submit. React apps usually handle submission themselves:

    tsx
    event.preventDefault();
    text
    Submit
    ↓
    preventDefault()
    ↓
    Validate
    ↓
    Send API request
    ↓
    Handle response
    ↓
    Update UI

    12. Complete Login Form

    tsx
    import { useState } from "react";
    function LoginForm() {
    const [form, setForm] = useState({
    email: "",
    password: ""
    });
    function handleChange(event) {
    const { name, value } = event.target;
    setForm((currentForm) => ({
    ...currentForm,
    [name]: value
    }));
    }
    function handleSubmit(event) {
    event.preventDefault();
    console.log(form);
    }
    return (
    <form onSubmit={handleSubmit}>
    <input
    type="email"
    name="email"
    value={form.email}
    onChange={handleChange}
    placeholder="Email"
    />
    <input
    type="password"
    name="password"
    value={form.password}
    onChange={handleChange}
    placeholder="Password"
    />
    <button type="submit">
    Login
    </button>
    </form>
    );
    }

    13. Labels and Accessibility

    Prefer linking labels with htmlFor and matching id:

    tsx
    <label htmlFor="email">
    Email
    </label>
    <input
    id="email"
    name="email"
    type="email"
    />
    text
    label htmlFor
    ↓
    input id

    14. Input Types

    tsx
    <input type="text" />
    <input type="email" />
    <input type="password" />
    <input type="number" />
    <input type="date" />
    <input type="tel" />
    <input type="url" />
    <input type="search" />

    Event handling is usually similar with onChange, but some controls expose different properties (e.g. checkboxes).

    15. Handling Number Inputs

    event.target.value is commonly a string (e.g. "25"). Convert intentionally when you need a number:

    tsx
    const age = Number(event.target.value);

    Be careful: Number("") is 0. Sometimes keep the raw string and convert during validation or submission.

    16. Handling Checkboxes

    Checkboxes use checked rather than value for their checked state.

    tsx
    import { useState } from "react";
    function Settings() {
    const [notifications, setNotifications] = useState(false);
    function handleChange(event) {
    setNotifications(event.target.checked);
    }
    return (
    <label>
    <input
    type="checkbox"
    checked={notifications}
    onChange={handleChange}
    />
    Enable notifications
    </label>
    );
    }
    text
    Text input → event.target.value
    Checkbox → event.target.checked

    17. Multiple Checkboxes

    Store selected values in an array:

    tsx
    const [skills, setSkills] = useState([]);
    <label>
    <input
    type="checkbox"
    value="React"
    checked={skills.includes("React")}
    onChange={handleSkillChange}
    />
    React
    </label>
    function handleSkillChange(event) {
    const { value, checked } = event.target;
    setSkills((currentSkills) => {
    if (checked) {
    return [...currentSkills, value];
    }
    return currentSkills.filter((skill) => skill !== value);
    });
    }

    18. Radio Buttons

    Radio buttons allow one choice from a group. Shared name groups them:

    tsx
    const [gender, setGender] = useState("");
    <label>
    <input
    type="radio"
    name="gender"
    value="male"
    checked={gender === "male"}
    onChange={(event) => setGender(event.target.value)}
    />
    Male
    </label>
    <label>
    <input
    type="radio"
    name="gender"
    value="female"
    checked={gender === "female"}
    onChange={(event) => setGender(event.target.value)}
    />
    Female
    </label>

    19. Select Dropdowns

    tsx
    const [country, setCountry] = useState("");
    <select
    value={country}
    onChange={(event) => setCountry(event.target.value)}
    >
    <option value="">Select country</option>
    <option value="india">India</option>
    <option value="usa">USA</option>
    <option value="uk">United Kingdom</option>
    </select>

    20. Textareas

    Control <textarea> with value and onChange (not children text like older HTML):

    tsx
    const [message, setMessage] = useState("");
    function handleChange(event) {
    setMessage(event.target.value);
    }
    <textarea
    value={message}
    onChange={handleChange}
    placeholder="Enter your message"
    />

    21. Form Validation

    tsx
    function handleSubmit(event) {
    event.preventDefault();
    if (!form.email) {
    console.log("Email is required");
    return;
    }
    if (!form.password) {
    console.log("Password is required");
    return;
    }
    console.log("Form is valid");
    }
    text
    Submit
    ↓
    Validate
    ↓
    ┌─────────────┐
    │ │
    Invalid Valid
    │ │
    ▼ ▼
    Show errors Submit

    22. Storing Validation Errors

    tsx
    const [errors, setErrors] = useState({});
    function validate() {
    const newErrors = {};
    if (!form.email) {
    newErrors.email = "Email is required";
    }
    if (!form.password) {
    newErrors.password = "Password is required";
    }
    return newErrors;
    }
    function handleSubmit(event) {
    event.preventDefault();
    const newErrors = validate();
    setErrors(newErrors);
    if (Object.keys(newErrors).length > 0) {
    return;
    }
    console.log("Submit form");
    }

    23. Displaying Validation Errors

    tsx
    <label htmlFor="email">
    Email
    </label>
    <input
    id="email"
    name="email"
    type="email"
    value={form.email}
    onChange={handleChange}
    />
    {errors.email && (
    <p>{errors.email}</p>
    )}

    This combines controlled inputs, state, event handling, conditional rendering, and validation.

    24. Validation on Change

    tsx
    function handleEmailChange(event) {
    const value = event.target.value;
    setEmail(value);
    if (!value) {
    setEmailError("Email is required");
    } else {
    setEmailError("");
    }
    }

    Validating every keystroke is not always best UX. Validate on change, blur, submit, or after submission attempts based on requirements.

    25. Validation on Blur

    onBlur fires when an input loses focus — useful so users finish typing before seeing errors:

    tsx
    <input
    value={email}
    onChange={handleChange}
    onBlur={handleBlur}
    />
    function handleBlur() {
    if (!email) {
    setError("Email is required");
    }
    }

    26. Disabling the Submit Button

    tsx
    <button
    type="submit"
    disabled={!form.email || !form.password}
    >
    Login
    </button>
    text
    Email empty OR Password empty → Button disabled
    Email ✓ and Password ✓ → Button enabled

    27. Form Submission and API Calls

    tsx
    async function handleSubmit(event) {
    event.preventDefault();
    const response = await fetch("/api/login", {
    method: "POST",
    headers: {
    "Content-Type": "application/json"
    },
    body: JSON.stringify(form)
    });
    const data = await response.json();
    console.log(data);
    }
    text
    Form → Submit → Validate → API request → Server → Response → Update UI

    28. Handling Loading State

    tsx
    const [isSubmitting, setIsSubmitting] = useState(false);
    async function handleSubmit(event) {
    event.preventDefault();
    setIsSubmitting(true);
    try {
    await saveData(form);
    } finally {
    setIsSubmitting(false);
    }
    }
    <button
    type="submit"
    disabled={isSubmitting}
    >
    {isSubmitting ? "Saving..." : "Save"}
    </button>

    29. Complete Registration Form

    tsx
    import { useState } from "react";
    function RegistrationForm() {
    const [form, setForm] = useState({
    name: "",
    email: "",
    password: ""
    });
    const [errors, setErrors] = useState({});
    function handleChange(event) {
    const { name, value } = event.target;
    setForm((currentForm) => ({
    ...currentForm,
    [name]: value
    }));
    }
    function handleSubmit(event) {
    event.preventDefault();
    const newErrors = {};
    if (!form.name.trim()) newErrors.name = "Name is required";
    if (!form.email.trim()) newErrors.email = "Email is required";
    if (!form.password) newErrors.password = "Password is required";
    setErrors(newErrors);
    if (Object.keys(newErrors).length > 0) return;
    console.log("Registration data:", form);
    }
    return (
    <form onSubmit={handleSubmit}>
    <div>
    <label htmlFor="name">Name</label>
    <input id="name" name="name" value={form.name} onChange={handleChange} />
    {errors.name && <p>{errors.name}</p>}
    </div>
    <div>
    <label htmlFor="email">Email</label>
    <input
    id="email"
    name="email"
    type="email"
    value={form.email}
    onChange={handleChange}
    />
    {errors.email && <p>{errors.email}</p>}
    </div>
    <div>
    <label htmlFor="password">Password</label>
    <input
    id="password"
    name="password"
    type="password"
    value={form.password}
    onChange={handleChange}
    />
    {errors.password && <p>{errors.password}</p>}
    </div>
    <button type="submit">Register</button>
    </form>
    );
    }

    30. Resetting a Form

    tsx
    function resetForm() {
    setForm({
    name: "",
    email: "",
    password: ""
    });
    setErrors({});
    }

    31. Form Submission Flow

    text
    User submits
    │
    ▼
    preventDefault()
    │
    ▼
    Validate
    │
    ┌──────┴──────┐
    │ │
    Invalid Valid
    │ │
    ▼ ▼
    Show errors Set loading
    │
    ▼
    API request
    │
    ┌─────────┴─────────┐
    │ │
    Success Error
    │ │
    ▼ ▼
    Show success Show error
    │
    ▼
    Reset / redirect

    32. Controlled vs Uncontrolled Components

    Controlled

    React state owns the value:

    tsx
    <input
    value={name}
    onChange={handleChange}
    />

    Uncontrolled

    The DOM maintains the value; React can access it with a ref:

    tsx
    <input ref={inputRef} />

    Prefer controlled when you need validation, conditional UI, resets, or other state-driven behavior.

    33. Using Refs with Forms

    tsx
    import { useRef } from "react";
    function Form() {
    const inputRef = useRef(null);
    function handleSubmit(event) {
    event.preventDefault();
    console.log(inputRef.current.value);
    }
    return (
    <form onSubmit={handleSubmit}>
    <input ref={inputRef} />
    <button type="submit">Submit</button>
    </form>
    );
    }

    Here the DOM holds the current value — React does not store it in state.

    34. When to Use Controlled Inputs

    Controlled inputs are especially useful when:

    • The value affects other UI
    • Validation is required
    • You need to reset fields programmatically
    • You disable/enable controls dynamically
    • You show live character counts or transform values
    • You need the current value elsewhere in the component
    tsx
    <p>
    Characters: {message.length}
    </p>

    35. Common Mistakes

    Mistake 1: Forgetting onChange

    tsx
    // Bad
    <input value={name} />
    // Better
    <input
    value={name}
    onChange={handleChange}
    />

    Mistake 2: Forgetting preventDefault()

    tsx
    function handleSubmit(event) {
    event.preventDefault();
    console.log("Submitted");
    }

    Mistake 3: Mutating the form object

    tsx
    // Avoid
    form.email = "new@example.com";
    // Instead
    setForm((currentForm) => ({
    ...currentForm,
    email: "new@example.com"
    }));

    Mistake 4: Forgetting name

    With one generic handleChange, inputs need matching name attributes.

    Mistake 5: Treating checkbox like text

    Use event.target.checked for checked state, not event.target.value.

    Mistake 6: Mixing controlled and uncontrolled accidentally

    Be consistent — do not start without a value and later switch to a state-driven value.

    36. Best Practices

    1. Use controlled components when the form needs React-driven behavior
    2. Keep form state organized (group related fields for larger forms)
    3. Use meaningful name attributes
    4. Use labels with htmlFor
    5. Validate intentionally — do not overwhelm users
    6. Show submission/loading state and prevent duplicate submits
    7. Keep the flow: Input → State → Validation → Submit → Result
    8. Preserve state immutability

    37. Practical Example — Contact Form

    tsx
    import { useState } from "react";
    function ContactForm() {
    const [form, setForm] = useState({
    name: "",
    email: "",
    message: ""
    });
    function handleChange(event) {
    const { name, value } = event.target;
    setForm((currentForm) => ({
    ...currentForm,
    [name]: value
    }));
    }
    function handleSubmit(event) {
    event.preventDefault();
    console.log("Contact form:", form);
    }
    return (
    <form onSubmit={handleSubmit}>
    <div>
    <label htmlFor="name">Name</label>
    <input id="name" name="name" value={form.name} onChange={handleChange} />
    </div>
    <div>
    <label htmlFor="email">Email</label>
    <input
    id="email"
    name="email"
    type="email"
    value={form.email}
    onChange={handleChange}
    />
    </div>
    <div>
    <label htmlFor="message">Message</label>
    <textarea
    id="message"
    name="message"
    value={form.message}
    onChange={handleChange}
    />
    </div>
    <button type="submit">Send Message</button>
    </form>
    );
    }

    All three fields share one handleChange — a core React form pattern.

    38. Event + State + Form Mental Model

    text
    USER
    │
    ▼
    Form Control
    │
    ▼
    onChange
    │
    ▼
    Event Handler
    │
    ▼
    setState()
    │
    ▼
    React State
    │
    ▼
    Component Render
    │
    ▼
    Updated Form
    text
    Submit → onSubmit → preventDefault() → Validate → API / Logic → Success or Error → Update UI

    39. Interview Questions

    1. What is a controlled component?

    A form element whose value is controlled by React state.

    tsx
    <input
    value={name}
    onChange={handleChange}
    />

    2. What is an uncontrolled component?

    The DOM maintains the form value, often accessed through a ref.

    3. Why is preventDefault() used?

    It prevents the browser's default form submission so the app can handle submission itself.

    4. How do you handle multiple inputs with one function?

    tsx
    function handleChange(event) {
    const { name, value } = event.target;
    setForm((currentForm) => ({
    ...currentForm,
    [name]: value
    }));
    }

    5. How do you handle a checkbox?

    Use event.target.checked and control with checked={value}.

    6. How do you handle a select?

    tsx
    <select
    value={country}
    onChange={handleChange}
    >

    7. How do you validate a form?

    tsx
    const errors = validate(form);
    if (Object.keys(errors).length > 0) {
    return;
    }

    8. value vs defaultValue?

    value for controlled inputs; defaultValue for initial uncontrolled values.

    9. checked vs defaultChecked?

    checked controls via React state; defaultChecked sets initial uncontrolled checkbox state.

    10. Why not mutate form state directly?

    Update through the setter so React can correctly schedule and reflect the change.

    40. Practice Exercises

    Exercise 1 — Name Form

    Name field + Submit; display the entered name after submission.

    Exercise 2 — Login Form

    Controlled email, password, and Login; print submitted values to the console.

    Exercise 3 — Registration Form

    Name, email, password, confirm password — validate required fields and matching passwords.

    Exercise 4 — Profile Form

    Name, email, age, country, bio — one state object and one handleChange.

    Exercise 5 — Checkbox

    Accept terms checkbox; disable Submit until selected.

    Exercise 6 — Radio Buttons

    Gender selector with radio buttons; only one option selectable.

    Exercise 7 — Skills

    Multiple checkboxes (JavaScript, React, TypeScript, Node.js); store selections in an array.

    Exercise 8 — Contact Form

    Name, email, message with validation and error messages under invalid fields.

    41. Quick Cheat Sheet

    RequirementReact Pattern
    Text / textarea / selectvalue + onChange
    Checkbox / radiochecked + onChange
    Form submitonSubmit
    Prevent browser submitevent.preventDefault()
    Input valueevent.target.value
    Checkbox stateevent.target.checked
    Field nameevent.target.name
    ControlledReact state owns value
    UncontrolledDOM owns value
    Initial uncontrolleddefaultValue / defaultChecked
    Reset formUpdate state to initial values

    Key Takeaway

    React forms are built around:

    text
    Form Input → Event → State → Validation → Submission → Application Logic → Updated UI

    The core pattern:

    tsx
    const [form, setForm] = useState({
    name: "",
    email: ""
    });
    function handleChange(event) {
    const { name, value } = event.target;
    setForm((currentForm) => ({
    ...currentForm,
    [name]: value
    }));
    }
    function handleSubmit(event) {
    event.preventDefault();
    // Validate and submit
    }
    <form onSubmit={handleSubmit}>
    <input
    name="name"
    value={form.name}
    onChange={handleChange}
    />
    <input
    name="email"
    value={form.email}
    onChange={handleChange}
    />
    <button type="submit">Submit</button>
    </form>

    In a controlled React form, state is the source of truth, onChange keeps state synchronized with user input, and onSubmit handles the final form action.

    Ready to mark this lesson complete?Track your journey across the entire course.