Forms are everywhere in modern web applications.
Whether you're building a login page, registration system, checkout process, search interface, or admin dashboard, you'll need to collect information from users and process it correctly.
In React, forms introduce several important concepts:
- Controlled and uncontrolled components.
- Managing input state.
- Handling form submissions.
- Validating user input.
- Displaying validation errors.
- Working with asynchronous operations.
- Fetching data from APIs.
- Handling loading and error states.
- Updating application data.
In this article, we'll explore these concepts step by step and build practical examples that demonstrate how real React applications handle forms and communicate with APIs.
1. Understanding Forms in React
In traditional HTML, a form manages its own input values through the DOM.
For example:
<form> <input type="text" name="username" /> <button type="submit"> Submit </button> </form> React provides a more flexible approach by allowing components to control input values through state.
This leads to two common approaches:
- Controlled components.
- Uncontrolled components.
Let's understand the difference.
2. Controlled Components
A controlled component is an input element whose value is controlled by React state.
Whenever the user changes the input, an event handler updates the state.
Basic Example
import { useState } from "react"; function UsernameForm() { const [username, setUsername] = useState(""); return ( <div> <input type="text" value={username} onChange={event => setUsername(event.target.value)} placeholder="Enter your username" /> <p>Username: {username}</p> </div> ); } export default UsernameForm; Let's break it down.
Step 1: Create state
const [username, setUsername] = useState(""); The input starts with an empty string.
Step 2: Connect state to the input
value={username} React controls the displayed value.
Step 3: Update state when the user types
onChange={event => setUsername(event.target.value)} Every change updates the React state.
Why Use Controlled Components?
Controlled components provide several advantages:
- You always know the current input value.
- You can validate input while the user types.
- You can conditionally disable buttons.
- You can dynamically change input values.
- You can easily reset forms.
- You can display real-time feedback.
They are especially useful for forms with validation and conditional behavior.
3. Handling Multiple Inputs
Real applications usually contain multiple input fields.
Instead of creating separate state variables for every input, you can store form data in an object.
import { useState } from "react"; function RegistrationForm() { const [formData, setFormData] = useState({ username: "", email: "", password: "" }); function handleChange(event) { const { name, value } = event.target; setFormData(previousData => ({ ...previousData, [name]: value })); } return ( <form> <input name="username" value={formData.username} onChange={handleChange} placeholder="Username" /> <input name="email" type="email" value={formData.email} onChange={handleChange} placeholder="Email" /> <input name="password" type="password" value={formData.password} onChange={handleChange} placeholder="Password" /> <pre>{JSON.stringify(formData, null, 2)}</pre> </form> ); } Understanding the Dynamic Property
This expression is especially important:
[name]: value It uses JavaScript's computed property syntax.
For example, if:
name = "email"; value = "alex@example.com"; Then:
{ ...previousData, [name]: value } becomes:
{ username: "Alex", email: "alex@example.com", password: "" } This pattern makes handling multiple inputs much easier.
4. Handling Form Submission
A form needs a submit handler.
In React, you can listen to the form's onSubmit event.
import { useState } from "react"; function LoginForm() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); function handleSubmit(event) { event.preventDefault(); console.log({ email, password }); } return ( <form onSubmit={handleSubmit}> <input type="email" value={email} onChange={event => setEmail(event.target.value)} placeholder="Email" required /> <input type="password" value={password} onChange={event => setPassword(event.target.value)} placeholder="Password" required /> <button type="submit"> Login </button> </form> ); } Why Use preventDefault()?
HTML forms normally perform a browser navigation or submission action.
In a typical React single-page application, you usually want to handle the submission using JavaScript.
That's why we use:
event.preventDefault(); It prevents the browser's default form submission behavior.
Why Use type="submit"?
A button inside a form can submit the form when its type is submit.
You can also use:
<button type="button"> Cancel </button> This button won't submit the form.
Being explicit about button types prevents unexpected form submissions.
5. Working with Different Input Types
React supports the standard HTML form controls.
Let's explore some common examples.
Text Input
<input type="text" value={name} onChange={event => setName(event.target.value)} /> Email Input
<input type="email" value={email} onChange={event => setEmail(event.target.value)} /> Password Input
<input type="password" value={password} onChange={event => setPassword(event.target.value)} /> Checkbox
Checkboxes use the checked property rather than value to control whether they are selected.
import { useState } from "react"; function TermsCheckbox() { const [accepted, setAccepted] = useState(false); return ( <label> <input type="checkbox" checked={accepted} onChange={event => setAccepted(event.target.checked)} /> I accept the terms and conditions. </label> ); } Notice:
event.target.checked This returns a boolean.
Radio Buttons
import { useState } from "react"; function GenderForm() { const [gender, setGender] = useState(""); return ( <div> <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> <p>Selected: {gender}</p> </div> ); } Select Dropdown
import { useState } from "react"; function CountrySelect() { const [country, setCountry] = useState(""); return ( <div> <select value={country} onChange={event => setCountry(event.target.value)} > <option value="">Select a country</option> <option value="germany">Germany</option> <option value="france">France</option> <option value="canada">Canada</option> </select> <p>Selected country: {country}</p> </div> ); } Textarea
<textarea value={message} onChange={event => setMessage(event.target.value)} placeholder="Write your message..." /> In React, use the value prop to control a textarea rather than placing its initial content between the opening and closing tags.
6. Uncontrolled Components
Unlike controlled components, uncontrolled components store their current values in the DOM.
React accesses those values when needed, commonly through refs.
Example
import { useRef } from "react"; function UncontrolledForm() { const usernameRef = useRef(null); function handleSubmit(event) { event.preventDefault(); console.log(usernameRef.current.value); } return ( <form onSubmit={handleSubmit}> <input ref={usernameRef} type="text" defaultValue="Alex" /> <button type="submit"> Submit </button> </form> ); } Here:
defaultValue="Alex" sets the initial value.
The DOM manages subsequent changes.
Controlled vs Uncontrolled Components
| Feature | Controlled | Uncontrolled |
|---|---|---|
| Value managed by | React state | DOM |
| Main API | value | defaultValue |
| Change handling | onChange | Optional |
| Access current value | State | Ref |
| Real-time validation | Easy | Requires additional handling |
| Dynamic behavior | Flexible | Less direct |
When Should You Use Each?
Use controlled components when:
- You need real-time validation.
- Input values affect other UI elements.
- You need conditional form behavior.
- You want predictable React state.
Use uncontrolled components when:
- You only need the values at submission time.
- You're integrating with traditional HTML forms.
- You're working with file inputs.
- You want a simpler form with minimal React state.
7. Form Validation
Validation ensures that users submit acceptable information.
For example:
- Email must be valid.
- Password must meet minimum requirements.
- Username cannot be empty.
- Required fields must be completed.
- Password confirmation must match.
Let's build a simple validation system.
Basic Validation Example
function validateForm(values) { const errors = {}; if (!values.username.trim()) { errors.username = "Username is required"; } if (!values.email.trim()) { errors.email = "Email is required"; } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) { errors.email = "Please enter a valid email"; } if (values.password.length < 8) { errors.password = "Password must contain at least 8 characters"; } return errors; } The function receives form values and returns an object containing validation errors.
If there are no errors, the returned object is empty.
Using Validation in a React Component
import { useState } from "react"; function RegistrationForm() { const [values, setValues] = useState({ username: "", email: "", password: "" }); const [errors, setErrors] = useState({}); function handleChange(event) { const { name, value } = event.target; setValues(previous => ({ ...previous, [name]: value })); } function handleSubmit(event) { event.preventDefault(); const validationErrors = validateForm(values); setErrors(validationErrors); if (Object.keys(validationErrors).length === 0) { console.log("Form submitted:", values); } } return ( <form onSubmit={handleSubmit}> <div> <input name="username" value={values.username} onChange={handleChange} placeholder="Username" /> {errors.username && ( <p>{errors.username}</p> )} </div> <div> <input name="email" type="email" value={values.email} onChange={handleChange} placeholder="Email" /> {errors.email && ( <p>{errors.email}</p> )} </div> <div> <input name="password" type="password" value={values.password} onChange={handleChange} placeholder="Password" /> {errors.password && ( <p>{errors.password}</p> )} </div> <button type="submit"> Register </button> </form> ); } This is a simple validation approach. In larger applications, you may want to validate fields as users interact with them and show errors only after a field has been touched or the form has been submitted.
Client-Side vs Server-Side Validation
Client-side validation improves the user experience.
However, it is not a security boundary.
Users can bypass browser validation and send requests directly to your server.
Always validate important data on the backend as well.
Never trust client-submitted information simply because it passed frontend validation.
8. Handling Asynchronous Form Submission
Many forms communicate with a backend.
For example:
- Login requests.
- Registration requests.
- Contact forms.
- Payment forms.
- Profile updates.
These operations are asynchronous.
You need to handle at least three states:
- Loading.
- Success.
- Error.
Example: Login Form
Related posts
Designing a Birth-Time Form That Does Not Invent an Astrology Chart
How Website Reliability Affects the Customer Journey
Daylight saving time is almost over. Here's what to know.
The accounts nobody remembers connecting to your site
GoHighLevel Automation Results: Benchmarks from 13 Real Builds (2026)
