New way to handle forms in Remix.run
Marcos Viana
Posted on April 23, 2024
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)
}
Posted on April 23, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.