DEV Community

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

Posted on

2

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

Image of Datadog

Master Mobile Monitoring for iOS Apps

Monitor your app’s health with real-time insights into crash-free rates, start times, and more. Optimize performance and prevent user churn by addressing critical issues like app hangs, and ANRs. Learn how to keep your iOS app running smoothly across all devices by downloading this eBook.

Get The eBook

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs