Basic Forms
Workplace Context
In nearly every web application, users need to provide input. Whether it is logging in, submitting contact information, configuring settings, or creating content, forms are the primary mechanism for collecting user data. As a React developer, understanding how to build robust and interactive forms is a fundamental skill. React provides a powerful way to manage form state and handle user input efficiently using the “controlled components” pattern.
Learning Objectives
By the end of this lesson, you will be able to:
- Explain the concept of controlled components in React forms.
- Implement controlled inputs for various types (text, textarea, checkbox, select).
- Handle form submission using the
onSubmit
event handler. - Prevent the default form submission behavior.
- Manage form state using the
useState
hook for single and multiple inputs. - Apply TypeScript for type safety in form elements and state.
Understanding Controlled Components
In standard HTML, form elements like <input>
, <textarea>
, and <select>
typically maintain their own state internally and update it based on user input. When you type into an input field, the browser handles the state update.
In React, the “controlled component” pattern flips this model. Instead of the DOM managing the input’s state, the React component’s state becomes the single source of truth.
- State Manages Value: The value of the form input (e.g., what is typed in an
<input>
) is driven by the React component’s state. We use thevalue
prop on the input element and link it to a state variable. - State Updates on Change: Any change to the input (e.g., typing a character) triggers an
onChange
event. We provide an event handler function to thisonChange
prop. - Handler Updates State: The
onChange
event handler updates the React component’s state with the new value from the input. - Re-render: React re-renders the component, and the input element receives the updated value from the state via its
value
prop.
This creates a loop: State -> Input Value -> Change Event -> Handler -> Update State -> Re-render. This gives React full control over the form element’s data.
Implementing a Simple Controlled Input
Let’s start with a basic text input.
Explanation:
- We initialize a state variable
name
usinguseState('')
. - The
<input>
element’svalue
prop is bound to thename
state variable. - The
onChange
prop is set to thehandleChange
function. handleChange
receives the event object (React.ChangeEvent<HTMLInputElement>
). We useevent.target.value
to get the current value of the input and update thename
state usingsetName
.- As the state changes, the component re-renders, and the input displays the value stored in the
name
state.
Handling Different Input Types
The controlled component pattern applies similarly to other form elements.
Textarea
// Example: Controlled Textarea
import React, { useState } from 'react';
const TextAreaForm: React.FC = () => {
const [message, setMessage] = useState<string>('');
const handleTextAreaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setMessage(event.target.value);
};
return (
<form>
<label htmlFor="messageArea">Message:</label>
<textarea
id="messageArea"
value={message}
onChange={handleTextAreaChange}
rows={4}
/>
<p>Message length: {message.length}</p>
</form>
);
};
export default TextAreaForm;
Note that for <textarea>
, we use the value
prop, just like <input>
, instead of setting content between the tags as in plain HTML. The event type is React.ChangeEvent<HTMLTextAreaElement>
.
Select Dropdown
// Example: Controlled Select
import React, { useState } from 'react';
const SelectForm: React.FC = () => {
const [selectedValue, setSelectedValue] = useState<string>('option2'); // Default value
const handleSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedValue(event.target.value);
};
return (
<form>
<label htmlFor="optionsSelect">Choose an option:</label>
<select id="optionsSelect" value={selectedValue} onChange={handleSelectChange}>
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
<option value="option3">Option 3</option>
</select>
<p>Selected: {selectedValue}</p>
</form>
);
};
export default SelectForm;
For <select>
, the value
prop on the select
element corresponds to the value
of the selected option
. The event type is React.ChangeEvent<HTMLSelectElement>
.
Checkbox
Checkboxes are slightly different because their state is usually boolean (true
/false
), and we check the checked
property instead of value
.
// Example: Controlled Checkbox
import React, { useState } from 'react';
const CheckboxForm: React.FC = () => {
const [isChecked, setIsChecked] = useState<boolean>(false);
const handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsChecked(event.target.checked); // Use event.target.checked
};
return (
<form>
<label>
<input
type="checkbox"
checked={isChecked} // Use 'checked' prop
onChange={handleCheckboxChange}
/>
Agree to terms
</label>
<p>Agreed: {isChecked ? 'Yes' : 'No'}</p>
</form>
);
};
export default CheckboxForm;
We use the checked
prop linked to state and event.target.checked
in the handler.
Handling Form Submission
Forms typically have a submit button. In many practical cases, we need to handle the submission event to process the collected data (e.g., send it to a server).
// Example: Handling Form Submission
import React, { useState } from 'react';
const SubmitForm: React.FC = () => {
const [username, setUsername] = useState<string>('');
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setUsername(event.target.value);
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); // VERY IMPORTANT!
alert(`Submitting username: ${username}`);
// Here you would typically send the data to an API
// e.g., fetch('/api/users', { method: 'POST', body: JSON.stringify({ username }) })
};
return (
<form onSubmit={handleSubmit}> {/* Attach handler to form's onSubmit */}
<label htmlFor="usernameInput">Username:</label>
<input
type="text"
id="usernameInput"
value={username}
onChange={handleChange}
/>
<button type="submit">Submit</button>
</form>
);
};
export default SubmitForm;
Key Points:
onSubmit
on<form>
: The event handler (handleSubmit
) is attached to the<form>
element’sonSubmit
prop, not the button’sonClick
. This catches submissions triggered by pressing Enter in an input field as well.event.preventDefault()
: This is crucial. By default, submitting an HTML form causes the browser to navigate to a new page or refresh the current one.event.preventDefault()
stops this default behavior, allowing our React code to handle the submission logic within the single-page application flow.- Event Type: The event type for form submission is
React.FormEvent<HTMLFormElement>
.
Managing State for Multiple Inputs
Forms often have multiple fields. How do we manage state for all of them?
Strategy 1: Individual useState
Calls
Use a separate useState
hook for each form field. This is simple and clear for smaller forms.
// Example: Multiple useState
import React, { useState } from 'react';
const MultiStateForm: React.FC = () => {
const [firstName, setFirstName] = useState<string>('');
const [lastName, setLastName] = useState<string>('');
const [email, setEmail] = useState<string>('');
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log('Submitting:', { firstName, lastName, email });
// API call logic...
};
return (
<form onSubmit={handleSubmit}>
<label>First Name:</label>
<input type="text" value={firstName} onChange={e => setFirstName(e.target.value)} />
<label>Last Name:</label>
<input type="text" value={lastName} onChange={e => setLastName(e.target.value)} />
<label>Email:</label>
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
<button type="submit">Register</button>
</form>
);
};
Strategy 2: Single State Object
Use a single useState
hook with an object containing all form fields. This can be more organized for larger forms but requires careful updating as highlighted below.
// Example: Single State Object
import React, { useState } from 'react';
interface FormData {
firstName: string;
lastName: string;
email: string;
}
const SingleStateForm: React.FC = () => {
const [formData, setFormData] = useState<FormData>({
firstName: '',
lastName: '',
email: '',
});
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target; // Destructure name and value
setFormData(prevFormData => ({
...prevFormData, // Spread existing state
[name]: value // Update changed field using computed property name
}));
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log('Submitting:', formData);
// API call logic...
};
return (
<form onSubmit={handleSubmit}>
<label>First Name:</label>
{/* IMPORTANT: Add 'name' attribute matching the state key */}
<input
type="text"
name="firstName"
value={formData.firstName}
onChange={handleChange}
/>
<label>Last Name:</label>
<input
type="text"
name="lastName"
value={formData.lastName}
onChange={handleChange}
/>
<label>Email:</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
<button type="submit">Register</button>
</form>
);
};
Key Aspects of Single State Object:
name
Attribute: Each input must have aname
attribute that matches the corresponding key in the state object (formData
).- Generic
handleChange
: We can use a singlehandleChange
function for all inputs. It usesevent.target.name
to determine which field to update. - Immutable Update: Inside
setFormData
, we use the spread operator (...prevFormData
) to copy the existing state and then update only the changed field using computed property names ([name]: value
). This ensures we do not mutate the state directly.
The choice between these strategies depends on the complexity of the form and personal/team preference.
Activity 1: Build a Contact Form
Setup
Use the editor below, which provides a basic React + TypeScript setup, or set up a similar environment on your own.
Task
Build a simple contact form component (ContactForm
) with the following fields:
- Name (text input)
- Email (email input)
- Message (textarea)
- A “Send Message” submit button
Implement the form using the controlled components pattern. Choose either the individual useState
or single state object strategy.
When the form is submitted:
- Prevent the default form submission.
- Use
console.log
to output the collected form data (name, email, message).
Code
Knowledge Check
Test your understanding of controlled components and form handling.
In the controlled component pattern for a text input in React, what is responsible for updating the input's visible value?
- Select an answer to view feedback.
Why is calling 'event.preventDefault()' typically necessary inside a form's 'onSubmit' handler in a React single-page application?
- Select an answer to view feedback.
When using a single state object to manage multiple form inputs with a generic 'handleChange' function, what HTML attribute is crucial for identifying which input triggered the change?
- Select an answer to view feedback.
Summary
This lesson introduced the fundamental concept of controlled components for handling forms in React. We learned that by linking input values to component state (value
prop) and updating that state via onChange
handlers, React gains full control over the form’s data. We explored how to implement this pattern for various input types (text, textarea, select, checkbox) and how to handle form submissions using onSubmit
while preventing the default browser behavior. We also discussed strategies for managing state for multiple inputs using either individual useState
calls or a single state object. Mastering forms is essential for building interactive user interfaces.
References
- React Documentation:
- MDN Web Docs:
Additional Resources
- React Hook Form - Popular form library with built-in validation and performance optimization
- Formik - Form library that helps with form state management and validation