Creating & Editing Contacts
20 min read
Build the Shared Contact Form
Build the Shared Contact Form
Both the New and Edit pages need the same fields — the only real
difference is whether the inputs start empty or pre-filled, and what the
submit button says. One component covers both.
Create components/ContactForm.tsx:
import type { Contact } from "@/lib/types";
type Props = {
action: (formData: FormData) => void;
contact?: Contact;
submitLabel: string;
};
export default function ContactForm({ action, contact, submitLabel }: Props) {
return (
<form action={action} className="form-card">
<div className="field">
<label htmlFor="name">Name</label>
<input id="name" name="name" type="text" defaultValue={contact?.name} placeholder="Jordan Blake" required />
</div>
<div className="field">
<label htmlFor="phone">Phone</label>
<input id="phone" name="phone" type="text" defaultValue={contact?.phone || ""} placeholder="(555) 010-2837" />
</div>
<div className="field">
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" defaultValue={contact?.email || ""} placeholder="jordan@example.com" />
</div>
<div className="field">
<label htmlFor="company">Company</label>
<input id="company" name="company" type="text" defaultValue={contact?.company || ""} placeholder="Optional" />
</div>
<div className="field">
<label htmlFor="notes">Notes</label>
<textarea id="notes" name="notes" defaultValue={contact?.notes || ""} placeholder="Optional" />
</div>
<label className="fav-toggle">
<input type="checkbox" name="favorite" defaultChecked={contact?.favorite} />
Mark as favorite
</label>
<div className="form-actions">
<a href={contact ? `/contacts/${contact.id}` : "/"} className="btn btn-ghost">Cancel</a>
<button type="submit" className="btn">{submitLabel}</button>
</div>
</form>
);
}
Two details worth calling out:
actionis typed as(formData: FormData) => void— that’s the shape
of a Server Action. The component doesn’t care whether it’s creating or
updating; that decision is made by whichever Server Action gets passed
in from the page.- Every input uses
defaultValue(ordefaultChecked), notvalue. This
is a plain, uncontrolled form — the browser owns the input state,
and there’s nouseStatetracking every keystroke. That’s only possible
because the form doesn’t need live validation or a controlled value;
it’s read once, on submit, viaFormData.
Checkpoint
The component compiles, but nothing renders it yet — that’s the final
lesson in this module.