CodingNic

Creating & Editing Contacts

Build the Shared Contact Form

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:

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:

  • action is 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 (or defaultChecked), not value. This
    is a plain, uncontrolled form — the browser owns the input state,
    and there’s no useState tracking every keystroke. That’s only possible
    because the form doesn’t need live validation or a controlled value;
    it’s read once, on submit, via FormData.

Checkpoint

The component compiles, but nothing renders it yet — that’s the final
lesson in this module.