Skip to Content
Lesson 8

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.

  1. 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 the value prop on the input element and link it to a state variable.
  2. 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 this onChange prop.
  3. Handler Updates State: The onChange event handler updates the React component’s state with the new value from the input.
  4. 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.

Editor
Loading...
Preview

Explanation:

  1. We initialize a state variable name using useState('').
  2. The <input> element’s value prop is bound to the name state variable.
  3. The onChange prop is set to the handleChange function.
  4. handleChange receives the event object (React.ChangeEvent<HTMLInputElement>). We use event.target.value to get the current value of the input and update the name state using setName.
  5. 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:

  1. onSubmit on <form>: The event handler (handleSubmit) is attached to the <form> element’s onSubmit prop, not the button’s onClick. This catches submissions triggered by pressing Enter in an input field as well.
  2. 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.
  3. 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:

  1. name Attribute: Each input must have a name attribute that matches the corresponding key in the state object (formData).
  2. Generic handleChange: We can use a single handleChange function for all inputs. It uses event.target.name to determine which field to update.
  3. 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:

  1. Prevent the default form submission.
  2. Use console.log to output the collected form data (name, email, message).

Code

Editor
Loading...
Preview

Activity 2: AI-Assisted Form Validation

Form validation is one of the most tedious yet critical aspects of form development. In industry, developers commonly use AI tools to generate validation logic, error messages, and regex patterns for common field types. This activity demonstrates how to leverage AI while maintaining responsibility for code correctness.

Requirements

You have been given the following validation requirements for a user registration form:

FieldValidation Rules
UsernameRequired. 3-20 characters. Only letters, numbers, and underscores. Must start with a letter.
EmailRequired. Must be a valid email format.
PasswordRequired. Minimum 8 characters. Must contain at least one uppercase letter, one lowercase letter, one number, and one special character (!@#$%^&*).
Confirm PasswordRequired. Must match the password field.
AgeOptional. If provided, must be a number between 13 and 120.
WebsiteOptional. If provided, must be a valid URL starting with http:// or https://.

Part 1: Manual Approach (10 minutes)

Create a validation function manually for one of the fields (choose Username or Password). Your function should:

  1. Accept the field value as a parameter
  2. Return an object with isValid: boolean and errorMessage: string | null
  3. Handle all the validation rules for that field
// Example structure: interface ValidationResult { isValid: boolean; errorMessage: string | null; } function validateUsername(username: string): ValidationResult { // Your implementation }

Part 2: AI-Assisted Generation (10 minutes)

Use an AI tool with the following prompt:

“Create a TypeScript validation module for a React registration form with these fields and rules: [paste the requirements table].

Requirements:

  1. Create a ValidationResult interface with isValid and errorMessage properties
  2. Create individual validation functions for each field
  3. Create a validateForm function that validates all fields and returns all errors
  4. Use clear, user-friendly error messages
  5. Include TypeScript types throughout
  6. Add comments explaining any regex patterns used”

Review the AI-generated code carefully.

Part 3: Critical Analysis (10 minutes)

Examine the AI-generated validation code:

Regex Patterns:

  • Can you understand what each regex pattern does?
  • Test the patterns with edge cases:
    • Does the email regex accept user@domain (no TLD)?
    • Does the password regex correctly require all character types?
    • Does the URL regex handle https://localhost:3000?

Error Messages:

  • Are the error messages specific enough to help users fix their input?
  • Would you modify any messages for better UX?

Edge Cases:

  • How does the validation handle empty strings vs. undefined?
  • What happens with whitespace-only input?
  • Does the age validation handle non-numeric strings?

Part 4: Integration (10 minutes)

Integrate the validated (and refined) validation logic into a controlled form component:

// Suggested structure interface RegistrationFormData { username: string; email: string; password: string; confirmPassword: string; age: string; website: string; } interface FormErrors { username?: string; email?: string; password?: string; confirmPassword?: string; age?: string; website?: string; } const RegistrationForm: React.FC = () => { const [formData, setFormData] = useState<RegistrationFormData>({ username: '', email: '', password: '', confirmPassword: '', age: '', website: '', }); const [errors, setErrors] = useState<FormErrors>({}); // Implement: handleChange, handleBlur (for field-level validation), handleSubmit return ( <form onSubmit={handleSubmit}> {/* Render fields with error messages */} </form> ); };

Reflection

Critical Thinking Questions:

  1. Trust but Verify: The AI-generated regex for email validation is likely RFC 5322 compliant, but can you explain what it’s checking? Would you deploy a regex you don’t fully understand?

  2. Context Matters: The AI doesn’t know your application’s specific requirements. What if your system needs to accept usernames with hyphens? How do you communicate edge cases to AI effectively?

  3. Maintenance: If another developer needs to modify the validation rules in 6 months, will they understand the AI-generated regex? How might you improve maintainability?

  4. Testing: What test cases would you write to verify the validation works correctly? Did the AI suggest any tests?

Important

Form validation is a prime example of where AI accelerates development without replacing developer judgment. While AI can generate regex patterns and boilerplate validation logic quickly, developers must:

  • Understand what the patterns actually validate
  • Test edge cases specific to their application
  • Ensure error messages match their UX guidelines
  • Consider accessibility (e.g., ARIA attributes for error states)

Many production applications use validation libraries like Zod, Yup, or React Hook Form that provide tested, documented validation patterns. AI can help you learn these libraries faster too!


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

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