New way to handle forms in Remix.run

marcosviana

Marcos Viana

Posted on April 23, 2024

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
💖 💪 🙅 🚩
marcosviana
Marcos Viana

Posted on April 23, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related