Skip to main content

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 useState hook.

🎯 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 profile state object to hold the data for all our form fields.
  • We have an errors state object to hold any validation errors.
  • We have placeholder handleChange and handleSubmit functions.

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, and checked from event.target.
  • For checkboxes, the value is in the checked property. For all other inputs, it's in the value property. We use a ternary operator to handle this.
  • We then update the profile state 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 (or checked) prop and the handleChange function.
  • We conditionally render error messages based on the errors state 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 validate function.
  • If there are errors, we update the errors state.
  • 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 handleChange function can be used to manage multiple input types, including text inputs, textareas, select dropdowns, and checkboxes.
  • Validation can be implemented by creating a separate validate function that is called in the handleSubmit function.
  • 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.

Further Reading