DEV Community

Cover image for New way to handle forms in Remix.run
Marcos Viana
Marcos Viana

Posted on

New way to handle forms in Remix.run

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 
Enter fullscreen mode Exit fullscreen mode

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) } 
Enter fullscreen mode Exit fullscreen mode

Top comments (0)