Skip to content

Instantly share code, notes, and snippets.

@0xMatt
Created July 1, 2025 16:50
Show Gist options
  • Save 0xMatt/48042ead019bba5c5c49d2b964d39bfc to your computer and use it in GitHub Desktop.
Save 0xMatt/48042ead019bba5c5c49d2b964d39bfc to your computer and use it in GitHub Desktop.
Component rendering twice
'use server';
import { ContactActionResponse, ContactForm, ContactSchema } from '@/app/contact/definitions';
export async function contact(
prevState: ContactActionResponse | null,
formData: FormData,
): Promise<ContactActionResponse> {
const rawData: ContactForm = {
name: formData.get('name') as string,
email: formData.get('email') as string,
message: formData.get('message') as string,
};
const validatedData = ContactSchema.safeParse(rawData);
if (!validatedData.success) {
return {
success: false,
message: 'Please fix the errors in the form',
errors: validatedData.error.flatten().fieldErrors,
payload: rawData,
};
}
return {
success: true,
message: 'Message sent successfully!',
payload: rawData,
};
}
'use client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Send } from 'lucide-react';
import { useActionState } from 'react';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import Form from 'next/form';
import { contact } from '@/app/contact/actions';
import { ContactActionResponse } from '@/app/contact/definitions';
import { toast } from 'sonner';
const initialState: ContactActionResponse = {
success: false,
message: '',
payload: null,
};
export default function ContactForm() {
const [state, action, pending] = useActionState(contact, initialState);
if (state?.success) {
toast.success('Message sent successfully');
}
console.log('state', state); Contact form rendering multiple times
return (
<>
<Form action={action}>
<div className="flex grow flex-col gap-5">
<div className="flex w-full flex-row gap-3">
<div className="w=full flex flex-col gap-3">
<Label htmlFor="name">Name</Label>
<Input id="name" type="text" name="name" defaultValue={state?.payload?.name} />
{state?.errors?.name && (
<p id="title-error" className="text-sm text-red-400">
{state.errors.name[0]}
</p>
)}
</div>
<div className="flex w-full flex-col gap-3">
<Label htmlFor="email">Email</Label>
<Input id="email" type="text" name="email" defaultValue={state?.payload?.email} />
{state?.errors?.email && (
<p id="title-error" className="text-sm text-red-400">
{state.errors.email[0]}
</p>
)}
</div>
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="message">Message</Label>
<Textarea id="name" name="message" defaultValue={state?.payload?.message} />
{state?.errors?.message && (
<p id="title-error" className="text-sm text-red-400">
{state.errors.message[0]}
</p>
)}
</div>
</div>
<Button type="submit" className="w-full">
<Send /> {pending ? 'Sending...' : 'Send'}
</Button>
</Form>
</>
);
}
import { z } from 'zod';
export interface ContactForm {
name: string;
email: string;
message: string;
}
export interface ContactActionResponse {
success: boolean;
message: string;
errors?: {
[K in keyof ContactForm]?: string[];
};
payload?: ContactForm | null;
}
export const ContactSchema = z.object({
name: z.string().min(2, {
message: 'Username must be at least 2 characters.',
}),
email: z.string().includes('@', {
message: 'Must use a valid email address.',
}),
message: z.string().min(10, {
message: 'Message must be at least 10 characters.',
}),
});
import ContactForm from './components/form';
export default function Page() {
return (
<>
<ContactForm />
</>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment