Topic 13 of 56 · Full Stack Advanced

Topic 13 : React Forms

Lesson TL;DRTopic 13: React Forms 📖 9 min read · 🎯 advanced · 🧭 Prerequisites: networking, generatingsignedapk Why this matters You've already built pages that look great — but here's the thing: most real apps...
9 min read·advanced·react · forms · controlled-components · form-validation

Topic 13: React Forms

📖 9 min read · 🎯 advanced · 🧭 Prerequisites: networking, generating-signed-apk

Why this matters

You've already built pages that look great — but here's the thing: most real apps need users to actually send something back. A login screen, a signup page, a checkout form, a search bar. The moment a user types into a field and clicks Submit, your app needs to capture that, validate it, and act on it. React gives you precise control over every keystroke, every selection, every submission — through controlled components, multi-input state, validation, and refs. Get this right, and you can handle almost any form in any app.

What You'll Learn

  • Create basic React forms with useState and controlled onChange handlers
  • Use a single state object to manage multiple form fields efficiently
  • Validate form data before submission and surface inline error messages
  • Access form element values directly using useRef as an uncontrolled alternative

The Analogy

Think of a React controlled form like a live court stenographer. Every word spoken in the courtroom is immediately transcribed — the stenographer's notebook (component state) is always in perfect sync with what was just said (the input value). If you want to know what was said at any moment, you check the notebook, not the speaker's memory. An uncontrolled form using refs, by contrast, is like asking a witness to recall their testimony only when the judge requests it — the data sits untouched in the DOM until you reach in and pull it out at submission time.

Chapter 1: Basic Form Handling

In React, forms look similar to plain HTML forms, but React hands you finer control over data and user interactions. The key difference is that each input's value is driven by component state, and every change fires a handler that updates that state — keeping React as the single source of truth.

src/components/SimpleForm.js

import React, { useState } from 'react';

function SimpleForm() {
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');

    const handleSubmit = (event) => {
        event.preventDefault();
        alert(`Name: ${name}, Email: ${email}`);
    };

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label>Name:</label>
                <input
                    type="text"
                    value={name}
                    onChange={(e) => setName(e.target.value)}
                />
            </div>
            <div>
                <label>Email:</label>
                <input
                    type="email"
                    value={email}
                    onChange={(e) => setEmail(e.target.value)}
                />
            </div>
            <button type="submit">Submit</button>
        </form>
    );
}

export default SimpleForm;

Integrate SimpleForm into your root component to see it in action:

src/App.js

import React from 'react';
import './App.css';
import SimpleForm from './components/SimpleForm';

function App() {
    return (
        <div className="App">
            <h1>React Forms</h1>
            <SimpleForm />
        </div>
    );
}

export default App;

Key points:

  • event.preventDefault() stops the browser's default page-reload behavior on form submit.
  • value={name} makes the input a controlled input — React owns the displayed value.
  • onChange fires on every keystroke, keeping state and UI in sync.

Chapter 2: Controlled Components

A controlled component is one where React state is the sole source of truth for the input's value. Rather than maintaining separate state variables for every field, the class graduates to a single formData object — one handleChange function dispatches updates to whichever field triggered the event by reading event.target.name.

src/components/ControlledForm.js

import React, { useState } from 'react';

function ControlledForm() {
    const [formData, setFormData] = useState({
        name: '',
        email: '',
        message: ''
    });

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

    const handleSubmit = (event) => {
        event.preventDefault();
        console.log('Form Data:', formData);
    };

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label>Name:</label>
                <input
                    type="text"
                    name="name"
                    value={formData.name}
                    onChange={handleChange}
                />
            </div>
            <div>
                <label>Email:</label>
                <input
                    type="email"
                    name="email"
                    value={formData.email}
                    onChange={handleChange}
                />
            </div>
            <div>
                <label>Message:</label>
                <textarea
                    name="message"
                    value={formData.message}
                    onChange={handleChange}
                ></textarea>
            </div>
            <button type="submit">Submit</button>
        </form>
    );
}

export default ControlledForm;

The spread ...formData preserves existing field values while the computed property [name]: value updates only the changed field. This pattern scales cleanly regardless of how many fields you add.

flowchart LR
    User["User types"] --> onChange["onChange fires"]
    onChange --> handler["handleChange reads\nevent.target.name + value"]
    handler --> setState["setFormData spreads\nexisting state + new value"]
    setState --> rerender["React re-renders\nwith updated value prop"]
    rerender --> input["<input value={...} />"]
    input --> User

Chapter 3: Handling Multiple Inputs

The same single-handler pattern extends naturally to every HTML form element type: text inputs, number inputs, and <select> dropdowns all emit event.target.name and event.target.value.

src/components/MultipleInputsForm.js

import React, { useState } from 'react';

function MultipleInputsForm() {
    const [formData, setFormData] = useState({
        username: '',
        age: '',
        gender: 'male'
    });

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

    const handleSubmit = (event) => {
        event.preventDefault();
        console.log('Form Data:', formData);
    };

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label>Username:</label>
                <input
                    type="text"
                    name="username"
                    value={formData.username}
                    onChange={handleChange}
                />
            </div>
            <div>
                <label>Age:</label>
                <input
                    type="number"
                    name="age"
                    value={formData.age}
                    onChange={handleChange}
                />
            </div>
            <div>
                <label>Gender:</label>
                <select
                    name="gender"
                    value={formData.gender}
                    onChange={handleChange}
                >
                    <option value="male">Male</option>
                    <option value="female">Female</option>
                    <option value="other">Other</option>
                </select>
            </div>
            <button type="submit">Submit</button>
        </form>
    );
}

export default MultipleInputsForm;

Notable patterns:

  • <select> is controlled the same way as <input> — bind value on the element, not selected on an <option>.
  • Setting gender: 'male' as the initial state pre-selects that option on first render.
  • type="number" inputs still return strings via event.target.value; parse with Number() or parseInt() if you need arithmetic.

Chapter 4: Form Validation

Submitting bad data is worse than not submitting at all. The class implements a validate() function that populates an errors object. If any errors exist, submission is blocked and inline messages are rendered next to the offending fields.

src/components/ValidatedForm.js

import React, { useState } from 'react';

function ValidatedForm() {
    const [formData, setFormData] = useState({
        email: '',
        password: ''
    });
    const [errors, setErrors] = useState({});

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

    const validate = () => {
        let tempErrors = {};

        if (!formData.email) {
            tempErrors.email = 'Email is required';
        } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
            tempErrors.email = 'Email is invalid';
        }

        if (!formData.password) {
            tempErrors.password = 'Password is required';
        } else if (formData.password.length < 6) {
            tempErrors.password = 'Password must be at least 6 characters';
        }

        setErrors(tempErrors);
        return Object.keys(tempErrors).length === 0;
    };

    const handleSubmit = (event) => {
        event.preventDefault();
        if (validate()) {
            console.log('Form Data:', formData);
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label>Email:</label>
                <input
                    type="email"
                    name="email"
                    value={formData.email}
                    onChange={handleChange}
                />
                {errors.email && <span className="error">{errors.email}</span>}
            </div>
            <div>
                <label>Password:</label>
                <input
                    type="password"
                    name="password"
                    value={formData.password}
                    onChange={handleChange}
                />
                {errors.password && <span className="error">{errors.password}</span>}
            </div>
            <button type="submit">Submit</button>
        </form>
    );
}

export default ValidatedForm;

Validation rules applied:

  • Email — required; must match the regex /\S+@\S+\.\S+/ (at least one non-whitespace character on each side of @ and .).
  • Password — required; minimum length of 6 characters.
  • validate() returns true only when tempErrors is empty, gating the submit logic.
  • {errors.email && <span>} short-circuits: the span only renders when an error string exists.

Chapter 5: Using Refs for Form Elements

Sometimes you don't want React managing every keystroke — you just need the value at submission time. useRef gives you a direct reference to a DOM node so you can read .current.value on demand. This is called an uncontrolled component pattern.

src/components/RefForm.js

import React, { useRef } from 'react';

function RefForm() {
    const nameRef = useRef(null);
    const emailRef = useRef(null);

    const handleSubmit = (event) => {
        event.preventDefault();
        const name = nameRef.current.value;
        const email = emailRef.current.value;
        alert(`Name: ${name}, Email: ${email}`);
    };

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label>Name:</label>
                <input type="text" ref={nameRef} />
            </div>
            <div>
                <label>Email:</label>
                <input type="email" ref={emailRef} />
            </div>
            <button type="submit">Submit</button>
        </form>
    );
}

export default RefForm;

When to prefer refs over controlled state:

  • Integrating with third-party DOM libraries that manage their own input values.
  • One-shot read (file inputs, focus management) where syncing state on every keystroke is unnecessary overhead.
  • The trade-off: you lose the ability to programmatically reset, validate on-the-fly, or derive UI from input values without an extra read.

🧪 Try It Yourself

Task: Build a RegistrationForm component that collects username, email, password, and confirmPassword. Add validation that:

  1. All fields are required.
  2. email passes the /\S+@\S+\.\S+/ test.
  3. password is at least 6 characters.
  4. confirmPassword matches password.

On successful validation, log the form data to the console. On failure, display inline error messages beneath each offending field.

Starter snippet:

import React, { useState } from 'react';

function RegistrationForm() {
    const [formData, setFormData] = useState({
        username: '',
        email: '',
        password: '',
        confirmPassword: ''
    });
    const [errors, setErrors] = useState({});

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

    const validate = () => {
        let tempErrors = {};
        // TODO: add your validation rules here
        setErrors(tempErrors);
        return Object.keys(tempErrors).length === 0;
    };

    const handleSubmit = (event) => {
        event.preventDefault();
        if (validate()) {
            console.log('Registered:', formData);
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            {/* TODO: render inputs for each field + error spans */}
            <button type="submit">Register</button>
        </form>
    );
}

export default RegistrationForm;

Success criterion: Open the browser console. A successful submission logs Registered: { username: '...', email: '...', ... }. Submitting with a mismatched password should show "Passwords do not match" beneath the confirmPassword field — no console log.

🔍 Checkpoint Quiz

Q1. What makes a form input a "controlled component" in React?

A) It uses a ref to read the DOM value at submit time
B) Its value prop is bound to component state and updated via onChange
C) It is wrapped in a <form> element
D) It uses defaultValue instead of value


Q2. Given this snippet, what appears in the console when the user types "Alice" into the name field and submits?

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

const handleChange = (event) => {
    const { name, value } = event.target;
    setFormData({ [name]: value }); // note: no spread
};

const handleSubmit = (event) => {
    event.preventDefault();
    console.log(formData);
};

A) { name: 'Alice', email: '' }
B) { name: 'Alice' }
C) { email: '' }
D) { name: '', email: '' }


Q3. In ValidatedForm, what does validate() return when both email and password are valid?

A) null
B) The formData object
C) true
D) false


Q4. You need to integrate a third-party rich-text editor that manages its own internal DOM state. Should you use a controlled component (state + onChange) or a useRef? Why?

🔍 Checkpoint Quiz — Answers

A1. B — A controlled component keeps value in sync with React state; every keystroke flows through onChangesetState → re-render.

A2. B — The handleChange uses { [name]: value } without spreading ...formData, so each change replaces the entire state object with only the updated field. After typing in the name field, email is gone from state.

A3. C — validate() returns Object.keys(tempErrors).length === 0, which is true when no errors were added to tempErrors.

A4. Use useRef. Third-party editors own their own internal state and often expose a getValue() API rather than firing native onChange events. Trying to control them via React state creates a two-source-of-truth conflict. A ref lets you reach in and read the value at submit time without fighting the library.

🪞 Recap

  • Controlled inputs bind value to React state and onChange to a state setter, making React the single source of truth for all form data.
  • A single handleChange using event.target.name and computed property syntax [name]: value handles any number of fields with no duplication.
  • <select> dropdowns follow the same controlled pattern as text inputs — bind value on the element, not selected on an option.
  • The validate() pattern populates an errors object and returns a boolean, cleanly separating validation logic from the submit handler.
  • useRef is the right tool when you need a one-shot DOM read at submit time and don't need React to track intermediate keystrokes.

📚 Further Reading

Like this topic? It’s one of 56 in Full Stack Advanced.

Block your seat for ₹2,500 and join the next cohort.