Controlled Components for Forms (Part 2): A Practical Example #74
📖 Introduction
Following our exploration of the theory behind Controlled Components for Forms (Part 1), this article delves into a practical, real-world example. We will build a more complex form that includes various input types and basic validation, solidifying your understanding of how to manage forms in React.
📚 Prerequisites
Before we begin, please ensure you have a solid grasp of the following concepts:
- All concepts from Part 1 of this series.
- JavaScript ES6 features (arrow functions, destructuring, computed property names).
- React state management with the
useStatehook.
🎯 Article Outline: What You'll Master
In this article, you will learn:
- ✅ Handling Multiple Input Types: How to manage text inputs, textareas, select dropdowns, and checkboxes in a single form.
- ✅ Implementing Basic Validation: How to add simple validation to your form and provide feedback to the user.
- ✅ Building a Complete Form: We will build a complete, practical example of a controlled form from scratch.
- ✅ Refining the User Experience: How to disable the submit button until the form is valid.
🧠 Section 1: The Project: A User Profile Form
For our practical example, we will build a "User Profile" form. This form will collect the following information:
- Username: A text input.
- Bio: A textarea for a short user biography.
- Role: A select dropdown for the user's role (e.g., Developer, Designer, Admin).
- Newsletter: A checkbox to subscribe to a newsletter.
We will also implement the following validation rules:
- The username cannot be empty.
- The bio must be at least 10 characters long.
💻 Section 2: Deep Dive - Building the Form
Let's start building our form.
2.1 - Initial State and Component Structure
First, let's set up our component and initialize the state for all our form fields. We'll also add a state for handling validation errors.
// code-block-1.jsx
import React, { useState } from 'react';
function UserProfileForm() {
const [profile, setProfile] = useState({
username: '',
bio: '',
role: 'Developer', // Default value for the select
newsletter: true, // Default value for the checkbox
});
const [errors, setErrors] = useState({});
const handleChange = (event) => {
// We'll implement this next
};
const handleSubmit = (event) => {
event.preventDefault();
// We'll implement this later
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields will go here */}
</form>
);
}
export default UserProfileForm;
Code Breakdown:
- We have a
profilestate object to hold the data for all our form fields. - We have an
errorsstate object to hold any validation errors. - We have placeholder
handleChangeandhandleSubmitfunctions.
2.2 - The Generic handleChange Function
Now, let's implement the handleChange function. This function will be responsible for updating the state for all our input types.
// code-block-2.jsx
const handleChange = (event) => {
const { name, value, type, checked } = event.target;
const newValue = type === 'checkbox' ? checked : value;
setProfile(prevProfile => ({
...prevProfile,
[name]: newValue,
}));
};
Code Breakdown:
- We destructure
name,value,type, andcheckedfromevent.target. - For checkboxes, the value is in the
checkedproperty. For all other inputs, it's in thevalueproperty. We use a ternary operator to handle this. - We then update the
profilestate using the computed property name[name]to set the correct property.
2.3 - Rendering the Form Fields
Now, let's add the JSX for our form fields.
// code-block-3.jsx
return (
<form onSubmit={handleSubmit}>
<div>
<label>
Username:
<input
type="text"
name="username"
value={profile.username}
onChange={handleChange}
/>
</label>
{errors.username && <p style={{ color: 'red' }}>{errors.username}</p>}
</div>
<div>
<label>
Bio:
<textarea
name="bio"
value={profile.bio}
onChange={handleChange}
/>
</label>
{errors.bio && <p style={{ color: 'red' }}>{errors.bio}</p>}
</div>
<div>
<label>
Role:
<select name="role" value={profile.role} onChange={handleChange}>
<option value="Developer">Developer</option>
<option value="Designer">Designer</option>
<option value="Admin">Admin</option>
</select>
</label>
</div>
<div>
<label>
<input
type="checkbox"
name="newsletter"
checked={profile.newsletter}
onChange={handleChange}
/>
Subscribe to our newsletter
</label>
</div>
<button type="submit">Save Profile</button>
</form>
);
Code Breakdown:
- Each input is connected to the state via its
value(orchecked) prop and thehandleChangefunction. - We conditionally render error messages based on the
errorsstate object.
🛠️ Section 3: Implementing Validation and Submission
Now, let's add the validation and form submission logic.
3.1 - The validate Function
We'll create a validate function that checks our form data and returns an object with any errors.
// code-block-4.jsx
const validate = () => {
const newErrors = {};
if (!profile.username) {
newErrors.username = 'Username is required';
}
if (profile.bio.length < 10) {
newErrors.bio = 'Bio must be at least 10 characters long';
}
return newErrors;
};
3.2 - The handleSubmit Function
Now, let's implement the handleSubmit function. This function will call our validate function and, if there are no errors, submit the form.
// code-block-5.jsx
const handleSubmit = (event) => {
event.preventDefault();
const newErrors = validate();
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
} else {
setErrors({});
console.log('Profile submitted:', profile);
alert('Profile saved successfully!');
}
};
Code Breakdown:
- We prevent the default form submission behavior.
- We call our
validatefunction. - If there are errors, we update the
errorsstate. - If there are no errors, we clear any existing errors and "submit" the form (in this case, by logging to the console and showing an alert).
3.3 - Disabling the Submit Button
Finally, let's improve the user experience by disabling the submit button if the form is invalid.
// code-block-6.jsx
const isFormValid = () => {
const newErrors = validate();
return Object.keys(newErrors).length === 0;
};
// ... inside the return statement
<button type="submit" disabled={!isFormValid()}>
Save Profile
</button>
Now, the "Save Profile" button will be disabled until the user has filled out the form correctly.
✨ Conclusion & Key Takeaways
In this article, we've built a practical, real-world example of a controlled form in React. We've seen how to handle various input types, implement basic validation, and provide feedback to the user.
Let's summarize the key takeaways:
- A single
handleChangefunction can be used to manage multiple input types, including text inputs, textareas, select dropdowns, and checkboxes. - Validation can be implemented by creating a separate
validatefunction that is called in thehandleSubmitfunction. - The user experience can be improved by providing clear error messages and disabling the submit button until the form is valid.
Challenge Yourself: To solidify your understanding, try to add a "password" and "confirm password" field to the form, with validation to ensure they match.
➡️ Next Steps
You now have a solid understanding of how to build and manage forms in React. In the next article, "Uncontrolled Components and useRef", we will explore an alternative way to handle forms and when it might be appropriate to use it.
Thank you for your dedication. Stay curious, and happy coding!
glossary
- Controlled Component: A form element whose value is controlled by React state.
- Validation: The process of ensuring that user input meets certain criteria.
- Computed Property Names: A feature in JavaScript that allows you to use an expression for a property name in an object literal.