Building the Authentication Form
Create the AuthForm component to handle the login and register flow
In the previous lesson, you built the login button and used it to guard the RSVPButton
for logged-out users.
In this lesson, you will create your first Form with shadcn/ui
- The AuthForm
component, which handles the login and register flow.
shadcn/ui
offers excellent form components built on top of react-hook-form
and zod
, and by the end of the lesson, you will be able to craft new forms based on it.
React Hook Form and Zod#
react-hook-form
is a library to build forms in React fast. It started as a react-only library, but over the years, it gained popularity, and it's now available as a generic and framework-agnostic @hook-form
library, which powers the react-hook-form library.
react-hook-form
is packed with features such as:
Easy Integration - you'll use
react-hook-form
with simple hooks and a minimal APIPerformance:
react-hook-form
minimizes re-renders and is optimized for performanceValidation: It has built-in integration with validation libraries such as
zod
oryup
zod
is a TypeScript-first schema declaration and validation library.
Given a schema definition, zod
will create a type that matches the schema and allow you to validate some input against the schema:
xxxxxxxxxx
import { z } from 'zod'
const schema = z.object({
username: z.string(),
password: z.string().min(6),
})
type User = z.infer<typeof schema>
const user: User = {
username: 'john'
password: '123456'
}
schema.parse(user)
It's a great fit for react-hook-form
as it allows you to define your form schema and use it to validate the user input.
Create the AuthForm
#
Here's how the AuthForm
will look like:

Begin by installing the Form
, Input
, and Tabs
components from shadcn/ui
:
xxxxxxxxxx
npx shadcn-ui@latest add Form input tabs
Next, create a new file at components/auth-form.tsx
and add its content. (A lot is going on inside. In the next section, you'll understand it by going over it step by step):
xxxxxxxxxx
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
import { useUser } from "@/hooks/UserContext"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "./ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
const formSchema = z.object({
username: z.string(),
password: z.string().min(6),
})
export const AuthForm = () => (
<Tabs defaultValue="register" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="register">Register</TabsTrigger>
<TabsTrigger value="login">Login</TabsTrigger>
</TabsList>
<TabsContent value="register">
<Card>
<CardHeader>
<CardTitle>Register</CardTitle>
<CardDescription>Join The Movment</CardDescription>
</CardHeader>
<CardContent>
<RegisterForm />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="login">
<Card>
<CardHeader>
<CardTitle>Login</CardTitle>
<CardDescription>Continue where you left</CardDescription>
</CardHeader>
<CardContent>
<LoginForm />
</CardContent>
</Card>
</TabsContent>
</Tabs>
)
export const RegisterForm = () => {
const { register } = useUser()
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
password: "",
},
})
const onSubmit = async (values: z.infer<typeof formSchema>) => {
const user = await register(values)
if (!user) {
form.setError("root", {
message: "Invalid username or password",
})
return
}
toast(`Registered successfully!`)
}
return (
<Form {form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Eyal" {field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input placeholder="shadcn" type="password" {field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>Submit</Button>
</form>
</Form>
)
}
export const LoginForm = () => {
const { login } = useUser()
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
password: "",
},
})
const onSubmit = async (values: z.infer<typeof formSchema>) => {
const response = await login(values)
if (response) {
form.setError("root", {
message: response,
})
return
}
toast.success(`Logged In successfully!`)
}
return (
<Form {form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Eyal" {field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input placeholder="shadcn" type="password" {field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
Submit
</Button>
<p className="h-4 font-bold text-red-500">
{form?.formState?.errors?.root?.message || ""}
</p>
</form>
</Form>
)
}
Step 1 - The Form Schema#
The first step is creating the schema for the Form using zod
.
react-hook-form
works with resolvers and schemas, such as Zod,
to validate user input and form data against predefined rules and ensure the inputs match the desired criteria.
In the AuthForm
case, the required inputs are username
and password
, and the schema is defined as follows:
xxxxxxxxxx
const formSchema = z.object({
username: z.string(),
password: z.string().min(6),
})
The password has a minimum length of 6 characters, and the username is required.
Thanks to the shadcn/ui
Form components, error messages are displayed when the user input doesn't match the schema. You'll see it in a minute.
Step 2 – The ‘AuthForm’ and ‘Tabs’ Components#
shadcn/ui
offers a very flexible tabs component. It consists of four parts:
Tabs: Main component for creating tabbed interfaces. It manages the state and relationships between parts.
TabsList: Container for
TabTrigger
components displays the tab list.TabTrigger: Individual tab buttons. Used for switching between tabs.
TabsPanel: The content area of each tab. It renders content related to the active tab.
The AuthForm
uses the Tabs
component to render its login and register tabs:

In the AuthForm
component, you first wrap the AuthForm
with the Tabs
component and set the default tab to register
.
Under it, you use the TabsList
component with TabTrigger
to show the Login
and Register
tabs.
xxxxxxxxxx
export const AuthForm = () => (
<Tabs defaultValue="register" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="register">Register</TabsTrigger>
<TabsTrigger value="login">Login</TabsTrigger>
</TabsList>
The TabsPanel
sibling TabsPanel
component for each TabTrigger
to show the content of each tab. You also wrap the forms with a Card
component to give it a nice look:
xxxxxxxxxx
<TabsContent value="register">
<Card>
<CardHeader>
<CardTitle>Register</CardTitle>
<CardDescription>Join The Movment</CardDescription>
</CardHeader>
<CardContent>
<RegisterForm />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="login">
<Card>
<CardHeader>
<CardTitle>Login</CardTitle>
<CardDescription>Continue where you left</CardDescription>
</CardHeader>
<CardContent>
<LoginForm />
</CardContent>
</Card>
</TabsContent>
Step 3 – The ‘Login’ and ‘Register’ Forms#
The login
and register
forms are very similar. They both use the Form
component from shadcn/ui
and the Input
component to handle the user input.
You start by creating the RegisterForm
:
xxxxxxxxxx
export const RegisterForm = () => {
const { register } = useUser()
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
password: "",
},
})
const onSubmit = async (values: z.infer<typeof formSchema>) => {
const user = await register(values)
if (!user) {
form.setError("root", {
message: "Invalid username or password",
})
return
}
toast(`Registered successfully!`)
}
return (
<Form {form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Eyal" {field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input placeholder="shadcn" type="password" {field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>Submit</Button>
</form>
</Form>
)
}
In the first line, you call the useUser
hook you've previously created to use the register function that will send a request to the server with the user input.
xxxxxxxxxx
const { register } = useUser()
Then, you've initiated the useForm
hook with a form configuration object.
This lesson preview is part of the Sleek Next.JS Applications with shadcn/ui course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to Sleek Next.JS Applications with shadcn/ui, plus 70+ \newline books, guides and courses with the \newline Pro subscription.
