Best way to handle forms in Remix.run

RMAG news

A good way to use forms in Remix is by using the remix-hook-form package, which utilizes the foundations of the react-hook-form package, considered the best for form handling.

Link to the documentation of the remix-hook-form package:
https://github.com/Code-Forge-Net/remix-hook-form

Since the form needs to be handled both on the server and client sides, the author Alem Tuzlak created this library.

I tested it and found it very good. Before testing it, I was planning to create my own package for form handling using any type of validator.

While I was looking for more information, I remembered the react-hook-form package and ended up stumbling upon the remix-hook-form package. My experience was the best.

First of all, you need to install the following packages:

pnpm add remix-hook-form react-hook-form @hookform/resolvers zod

Below is a page that handles multiple forms and performs validation on both the client and server sides.

I used intent to differentiate the forms in a single route. I used the Form component and also utilized fetcher.

import type { ActionFunctionArgs, MetaFunction } from @remix-run/node;
import { json } from @remix-run/node
import { Form, useFetcher, useNavigation, isRouteErrorResponse, useRouteError } from @remix-run/react

import { useRemixForm, getValidatedFormData, parseFormData } from remix-hook-form
import { zodResolver } from @hookform/resolvers/zod
import { z } from zod

import { Label, Input, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from @workspace/ui
import { Loader2Icon } from lucide-react

export const meta: MetaFunction = () => [{ title: Summary }];

export default function Page() {
const fetcher = useFetcher()
const navigation = useNavigation()

const loginForm = useRemixForm<LoginFormData>({
mode: onSubmit,
resolver: loginResolver,
submitData: { intent: login },
fetcher
})

const signUpForm = useRemixForm<SignUpFormData>({
mode: onSubmit,
resolver: signUpResolver,
submitData: { intent: sign-up },
})

return (
<div className=“w-full min-h-dvh flex items-center justify-center”>
<div className=“max-w-sm space-y-5”>
<fetcher.Form onSubmit={loginForm.handleSubmit}>
<Card className=“w-full”>
<CardHeader>
<CardTitle className=“text-2xl”>Login</CardTitle>
<CardDescription>
Enter your email below to login to your account.
</CardDescription>
</CardHeader>
<CardContent className=“grid gap-4”>
<div className=“grid gap-2”>
<Label htmlFor=“email”>Email</Label>
<Input id=“email” type=“email” placeholder=“m@example.com” {loginForm.register(email)} />
{loginForm.formState.errors.email && (
<p className=“text-xs text-red-500 font-medium”>{loginForm.formState.errors.email.message}</p>
)}
</div>
<div className=“grid gap-2”>
<Label htmlFor=“password”>Password</Label>
<Input id=“password” type=“password” {loginForm.register(password)}/>
{loginForm.formState.errors.password && (
<p className=“text-xs text-red-500 font-medium”>{loginForm.formState.errors.password.message}</p>
)}
</div>
</CardContent>
<CardFooter>
<Button type=“submit” className=“w-full”>
{(fetcher.formData?.get(intent) === “login”)
? <Loader2Icon className=“w-4 h-4 animate-spin” />
: Sign In
}
</Button>
</CardFooter>
</Card>
</fetcher.Form>

<Form onSubmit={signUpForm.handleSubmit}>
<Card className=“w-full”>
<CardHeader>
<CardTitle className=“text-2xl”>SignUp</CardTitle>
<CardDescription>
Enter your email below to create your account.
</CardDescription>
</CardHeader>
<CardContent className=“grid gap-4”>
<div className=“grid gap-2”>
<Label htmlFor=“email”>Email</Label>
<Input id=“email” type=“email” placeholder=“m@example.com” {signUpForm.register(email)} />
{signUpForm.formState.errors.email && (
<p className=“text-xs text-red-500 font-medium”>{signUpForm.formState.errors.email.message}</p>
)}
</div>
</CardContent>
<CardFooter>
<Button type=“submit” className=“w-full”>
{(navigation.formData?.get(intent) === “sign-up”)
? <Loader2Icon className=“w-4 h-4 animate-spin” />
: Sign up
}
</Button>
</CardFooter>
</Card>
</Form>
</div>
</div>
)
}

export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await parseFormData<{ intent?: string }>(request.clone())
if (!formData.intent) throw json({ error: Intent not found }, { status: 404 })
switch (formData.intent) {
case sign-up: return await handleSignUp(request)
case login: return await handleLogin(request)
default: throw json({ error: Invalid intent }, { status: 404 })
}
}

const loginSchema = z.object({ email: z.string().email(), password: z.string().min(8) })
const loginResolver = zodResolver(loginSchema)
type LoginFormData = z.infer<typeof loginSchema>
async function handleLogin(request: Request) {
const { errors, data, receivedValues: defaultValues } =
await getValidatedFormData<LoginFormData>(request, loginResolver);
if (errors) return json({ errors, defaultValues })
await new Promise(resolve => setTimeout(resolve, 1500))
return json(data)
}

const signUpSchema = z.object({ email: z.string().email() })
const signUpResolver = zodResolver(signUpSchema)
type SignUpFormData = z.infer<typeof signUpSchema>
async function handleSignUp(request: Request) {
const { errors, data, receivedValues: defaultValues } =
await getValidatedFormData<SignUpFormData>(request, signUpResolver);
if (errors) return json({ errors, defaultValues })
await new Promise(resolve => setTimeout(resolve, 1500))
return json(data)
}

Leave a Reply

Your email address will not be published. Required fields are marked *