New way to handle forms in Remix.run
Tags: remix, typescript, fullstack, form
Published: 2024-04-23 13:10:26

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:

```shell

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.

```tsx
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)
}
```