DEV Community

Cover image for Stop Writing Messy Forms — Use This Modern Next.js Validation Stack
Marwan Zaky
Marwan Zaky

Posted on • Edited on

Stop Writing Messy Forms — Use This Modern Next.js Validation Stack

Step by step on how to implement a modern Next.js form validation stack:

  • react-form-hook manages the state, submission, and error tracking
  • shadcn provides the pre-styled accessible components (e.g. Card, Input, Field) that tie into the hook.
  • zod defines the "shape" of your data and validation rules

Demo example: https://mamolio.vercel.app/signin

Demo code: https://github.com/marwanzaky/mern-ecommerce


1. Install Dependencies

Go to your existing Next.js app directory and install react-form-hook and zod

npm install react-hook-form zod @hookform/resolvers
Enter fullscreen mode Exit fullscreen mode

2. Install Shadcn Components

If you don't have shadcn installed already use this command

npx shadcn@latest init --preset b0 --template next
Enter fullscreen mode Exit fullscreen mode

Then use this to add input, field, and card components

npx shadcn@latest add input
npx shadcn@latest add field
npx shadcn@latest add card
Enter fullscreen mode Exit fullscreen mode

3. Login Form Page

Create new page app/signin/page.tsx and import card, field, button, input and zod components

// app/signin/page.tsx
"use client";

import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";

import {
    Card,
    CardContent,
    CardDescription,
    CardHeader,
    CardTitle,
} from "@shadcn/components/ui/card";
import {
    Field,
    FieldDescription,
    FieldError,
    FieldGroup,
    FieldLabel,
} from "@components/ui/field";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
Enter fullscreen mode Exit fullscreen mode

Create sign up form schema

// app/signin/page.tsx
const SignUpSchema = z.object({
    email: z
        .email()
        .nonempty("This field is required.")
        .max(32, "Email is too short."),
    password: z.string().nonempty("This field is required."),
});

type SignUpInput = z.infer<typeof SignUpSchema>;
Enter fullscreen mode Exit fullscreen mode

Create react-form-hook

// app/signin/page.tsx
export default function Page() {
    const {
        register,
        handleSubmit,
        formState: { errors, isSubmitting },
    } = useForm<SignUpInput>({
        resolver: zodResolver(SignUpSchema),
        mode: "onSubmit",
        defaultValues: {
            email: "",
            password: "",
        },
    });

    const onSubmit = async (data: SignUpInput) => {
        const { email, password } = data;
        console.log("email", email);
        console.log("password", password);
    };
}
Enter fullscreen mode Exit fullscreen mode

Example on how to use them to create fields

<Field data-invalid={!!errors.email}>
    <FieldLabel htmlFor="email">Email</FieldLabel>
    <FieldContent>
        <Input
            id="email"
            placeholder="m@example.com"
            aria-invalid={!!errors.email}
            {...register("email")}
        />
    </FieldContent>
    <FieldError>{errors.email?.message}</FieldError>
</Field>
Enter fullscreen mode Exit fullscreen mode

Now let’s create card form with email and password fields and submit button

// app/signin/page.tsx
export default function Page() {
    return (
        <Card className="mx-auto w-full max-w-sm">
            <CardHeader>
                <CardTitle>Login to your account</CardTitle>
                <CardDescription>
                    Enter your email below to login to your account
                </CardDescription>
            </CardHeader>

            <CardContent>
                <form onSubmit={form.handleSubmit(onSubmit)}>
                    <FieldGroup>
                        <Field data-invalid={!!errors.email}>
                            <FieldLabel htmlFor="email">Email</FieldLabel>
                            <FieldContent>
                                <Input
                                    id="email"
                                    placeholder="m@example.com"
                                    aria-invalid={!!errors.email}
                                    {...register("email")}
                                />
                            </FieldContent>
                            <FieldError>{errors.email?.message}</FieldError>
                        </Field>

                        <Field data-invalid={!!errors.password}>
                            <div className="flex items-center">
                                <FieldLabel htmlFor="password">
                                    Password
                                </FieldLabel>
                                <Link
                                    href="/forgot-password"
                                    className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
                                >
                                    Forgot your password?
                                </Link>
                            </div>
                            <FieldContent>
                                <Input
                                    id="password"
                                    type="password"
                                    aria-invalid={!!errors.password}
                                    {...register("password")}
                                />
                            </FieldContent>
                            <FieldError>{errors.password?.message}</FieldError>
                        </Field>

                        <Field>
                            <Button type="submit">Login</Button>
                            <Button type="button" variant="outline">
                                Login with Google
                            </Button>
                            <FieldDescription className="text-center">
                                Don&apos;t have an account?{" "}
                                <Link href="/signup">Sign up</Link>
                            </FieldDescription>
                        </Field>
                    </FieldGroup>
                </form>
            </CardContent>
        </Card>
    );
}
Enter fullscreen mode Exit fullscreen mode

4. Why this stack works well

  • react-hook-form → minimal re-renders, great performance
  • zod → schema = validation + types in one place
  • shadcn → accessible, composable UI components

You are all set to use form validation for anything really

More live examples

Feel free to review the code on GitHub

You may also want to learn How to Implement Google OAuth 2.0 in Next.js with NestJS

Top comments (0)