1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-10-30 03:16:03 -04:00

Refactor contact form validation to use useDebounce for improved performance and streamline error handling. Simplify error retrieval logic and ensure touched state is updated on input changes.

This commit is contained in:
2025-09-02 19:10:56 -04:00
parent 20d8aad41f
commit b06e7bbf4d
3 changed files with 29 additions and 15 deletions

View File

@@ -1,6 +1,7 @@
"use client";
import { useActionState, useEffect, useState } from "react";
import { useActionState, useState } from "react";
import { useDebounce } from "react-use";
import { SendIcon, Loader2Icon, CheckIcon, XIcon } from "lucide-react";
import Link from "@/components/link";
import Input from "@/components/ui/input";
@@ -8,8 +9,8 @@ import Textarea from "@/components/ui/textarea";
import Button from "@/components/ui/button";
import { MarkdownIcon } from "@/components/icons";
import { cn } from "@/lib/utils";
import { send, type ContactState } from "@/lib/server/resend";
import { ContactSchema, type ContactInput } from "@/lib/validation/contact";
import { send, type ContactState } from "@/lib/server/contact";
import { ContactSchema, type ContactInput } from "@/lib/schemas/contact";
const ContactForm = () => {
const [formState, formAction, pending] = useActionState<ContactState, FormData>(send, {
@@ -24,27 +25,37 @@ const ContactForm = () => {
message: "",
});
// client-side validation using shared schema
const [clientErrors, setClientErrors] = useState<Partial<Record<keyof ContactInput, string[]>>>({});
const [touched, setTouched] = useState<{ name: boolean; email: boolean; message: boolean }>({
// keep track of which fields have been touched
const [touched, setTouched] = useState<Record<keyof ContactInput, boolean>>({
name: false,
email: false,
message: false,
});
useEffect(() => {
const id = setTimeout(() => {
// client-side validation using shared schema
const [clientErrors, setClientErrors] = useState<Partial<Record<keyof ContactInput, string[]>>>({});
useDebounce(
() => {
const result = ContactSchema.safeParse(formFields);
setClientErrors(result.success ? {} : result.error.flatten().fieldErrors);
}, 200);
return () => clearTimeout(id);
}, [formFields]);
},
150,
[formFields]
);
const hasClientErrors = Object.values(clientErrors).some((errs) => (errs?.length || 0) > 0);
const nameError = (touched.name ? clientErrors.name?.[0] : undefined) ?? formState.errors?.name?.[0];
const emailError = (touched.email ? clientErrors.email?.[0] : undefined) ?? formState.errors?.email?.[0];
const messageError = (touched.message ? clientErrors.message?.[0] : undefined) ?? formState.errors?.message?.[0];
const getErrorForField = (field: keyof ContactInput): string | undefined => {
const ce = clientErrors[field]?.[0];
if (ce) return ce;
if (!touched[field]) return formState.errors?.[field]?.[0];
return undefined;
};
const nameError = getErrorForField("name");
const emailError = getErrorForField("email");
const messageError = getErrorForField("message");
return (
<form action={formAction} className="my-6 space-y-4">
@@ -56,6 +67,7 @@ const ContactForm = () => {
value={formFields.name}
onChange={(e) => {
setFormFields({ ...formFields, name: e.target.value });
setTouched((t) => ({ ...t, name: true }));
}}
onBlur={() => setTouched((t) => ({ ...t, name: true }))}
disabled={pending || formState.success}
@@ -73,6 +85,7 @@ const ContactForm = () => {
value={formFields.email}
onChange={(e) => {
setFormFields({ ...formFields, email: e.target.value });
setTouched((t) => ({ ...t, email: true }));
}}
onBlur={() => setTouched((t) => ({ ...t, email: true }))}
disabled={pending || formState.success}
@@ -88,6 +101,7 @@ const ContactForm = () => {
value={formFields.message}
onChange={(e) => {
setFormFields({ ...formFields, message: e.target.value });
setTouched((t) => ({ ...t, message: true }));
}}
onBlur={() => setTouched((t) => ({ ...t, message: true }))}
disabled={pending || formState.success}

View File

@@ -2,7 +2,7 @@
import { env } from "@/lib/env";
import { Resend } from "resend";
import { ContactSchema } from "@/lib/validation/contact";
import { ContactSchema } from "@/lib/schemas/contact";
import siteConfig from "@/lib/config/site";
import { checkBotId } from "botid/server";