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

Understanding the Input Component Step by Step

Building the TaskForm Component

Now that you have a reusable input, let’s build the form that will use it.

Here’s the code:

'use client';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';

const schema = z.object({
  title: z.string().trim().min(1, 'Title is required'),
  content: z.string().trim().min(1, 'Content is required'),
  dueDate: z.string().optional(),
});

export type TaskFormValues = z.infer<typeof schema>;

This defines the validation schema with Zod:

  • title and content are required fields.
  • dueDate is optional.

By using zodResolver, React Hook Form will automatically check these rules every time the user submits.

Setting Up React Hook Form

const {
  register,
  handleSubmit,
  formState: { errors, isSubmitting },
} = useForm<TaskFormValues>({
  resolver: zodResolver(schema),
  defaultValues: { title: '', content: '', dueDate: '', ...initialValues },
});

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.
  • errors stores any validation messages generated by Zod.
  • isSubmitting tells us if the form is currently being submitted — useful for disabling buttons.
  • defaultValues sets 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; initialValues is included now to prepare for a later edit flow.

Using the Input Component and Validation Together

<Input label="Title" id="title" placeholder="Task title" {...register('title')} />
{errors.title && <p className="mt-1 text-sm text-red-600">{errors.title.message}</p>}

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.message and 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

<form onSubmit={handleSubmit(async (v) => { await onSubmit(v); })}>
  <Button type="submit" disabled={isSubmitting}>{submitLabel}</Button>
</form>
  • handleSubmit runs validation automatically.
  • If validation passes, it calls your onSubmit function with the form data.
  • While submitting, isSubmitting disables the button to prevent multiple submissions.
  • submitLabel lets 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:

"use client";
import { TaskForm, TaskFormValues } from '@/components/tasks/TaskForm';

export default function NewTaskPage() {

  const onSubmit = async (values: TaskFormValues) => {
    alert("No action for now. Will be implemented in later units");
  };

  return (
    <div className="space-y-6">
      <h1 className="text-2xl font-semibold">Create Task</h1>
      <div className="rounded-lg border bg-white p-4">
        <TaskForm onSubmit={onSubmit} submitLabel="Create" />
      </div>
    </div>
  );
}

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, and handleSubmit work 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.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal