Reusable Forms and Validation
Reusable Forms and Validation
Welcome to your first lesson in the course Forms, Validation & Creating Tasks.
In this lesson, you’ll move from displaying data to collecting it through forms. Forms are the bridge between users and your app — they let users create, edit, or share information.
But forms can get messy if you repeat code for every field or skip validation. That’s why we’ll learn two important patterns:
- Building a reusable input component, so all your text fields look and behave consistently.
- Using validation, so users can’t submit incomplete or incorrect information.
By the end of this lesson, you’ll have a clean, reusable Input component and a TaskForm that validates data before submitting it.
Quick Recap: Where We Are
So far in your project, you’ve built a functional frontend structure.
You already have:
- A dashboard layout with a header and sidebar navigation (in
src/app/(dashboard)/layout.tsx). - A dashboard page that shows task statistics (total, completed, incomplete).
- A tasks list page that displays existing tasks in a clean, read-only list.
- Reusable UI pieces like the Button and TaskRow components.
Up until now, the user experience has been read-only — users can see their tasks but not create new ones.
In this lesson, we’ll change that by building a reusable form for creating tasks.
Building a Reusable Input Component
Most forms include several input fields (text, date, email, etc.), and each one needs labels, styling, and error handling. Instead of rewriting that for every field, you can build a single reusable Input component.
Here’s the code:
Understanding the Input Component Step by Step
Props
- The component accepts all normal HTML input attributes (like
type,placeholder,onChange, etc.) thanks toInputHTMLAttributes<HTMLInputElement>. - It also adds two optional custom props:
label— the text displayed above the input.error— the validation message displayed below the input.
This makes the component flexible and reusable for any kind of text input.
forwardRef
We wrap the component in forwardRef so we can pass a ref to the <input> element.
This is essential for libraries like React Hook Form, which use refs to control the field’s value and track its state.
Without forwardRef, our input wouldn’t connect properly to form management tools.
Base Styles
The base variable defines styles that every input should share — full width, padding, rounded corners, and focus outlines.
Normal and Danger States
The normal and danger constants define how the input should look depending on whether there’s a validation error.
- The normal style uses a gray border and blue focus ring.
- The danger style switches to red when an error exists.
The component picks which one to use dynamically:
If error is truthy, the danger style applies.
Label and Error Messages
- If a
labelprop is provided, the component renders a<label>element withhtmlFor={id}to connect it to the input for accessibility. - If an
errormessage exists, it shows below the field in red text.
Example Usage: <Input label="Title" id="title" placeholder="Task title" error="Title is required" />
This creates:
- A labeled input with “Title” above it.
- A red border and message “Title is required” below it.
Building the TaskForm Component
Now that you have a reusable input, let’s build the form that will use it.
Here’s the code:
This defines the validation schema with Zod:
titleandcontentare required fields.dueDateis optional.
By using zodResolver, React Hook Form will automatically check these rules every time the user submits.
Setting Up React Hook Form
Here’s what each part does:
useForm()initializes form state.register()connects input fields to the form’s tracking system.handleSubmit()handles validation and calls your submit function only if validation passes.errorsstores any validation messages generated by Zod.isSubmittingtells us if the form is currently being submitted — useful for disabling buttons.defaultValuessets initial field values, so the form can be reused for both creating and editing tasks. In this unit, you only use the form for creating tasks;initialValuesis included now to prepare for a later edit flow.
Using the Input Component and Validation Together
Here’s what’s happening:
register('title')links this field to React Hook Form and the Zod schema.- If the user leaves it empty, Zod will generate an error (“Title is required”).
- That message is stored in
errors.title.messageand displayed below the field.
The same logic applies for content and dueDate.
The form also includes a <textarea> for task content and uses the Input component again for the due date (this time with type="date").
Handling Submission
handleSubmitruns validation automatically.- If validation passes, it calls your
onSubmitfunction with the form data. - While submitting,
isSubmittingdisables the button to prevent multiple submissions. submitLabellets you customize the button text (e.g., “Save”, “Create”).
Why Use Zod and React Hook Form Together
- Zod defines your rules.
- React Hook Form enforces them, handles state, and manages inputs efficiently.
Zod ensures your form’s data is always valid.
React Hook Form ensures your UI stays performant — it doesn’t re-render the entire form on every keystroke.
The combination keeps your code type-safe, compact, and easy to maintain.
The “New Task” Page
Now that the TaskForm is ready, it’s used in a new page under src/app/(dashboard)/tasks/new/page.tsx:
For now, when you submit, it simply shows an alert.
In future lessons, this will connect to your backend API to actually create a new task.
Summary
In this lesson, you learned how to:
- Create a reusable Input component that handles labels, styling, and validation states.
- Build a TaskForm that collects task information and validates it using React Hook Form and Zod.
- Understand how
register,errors, andhandleSubmitwork together to manage form logic. - Set up the structure for your New Task page, ready for backend integration in upcoming units.
By combining reusability with strong validation, you’re laying the foundation for robust, user-friendly forms throughout your Next.js app.
