Controlled Components

Our boards display kudos, but there is no way to add new ones. Let’s build a form for that.

Controlled vs. uncontrolled inputs

In plain HTML, form inputs manage their own state. When you type into an <input>, the browser tracks the value. You only read it when the form is submitted. In React we call this an uncontrolled input, because the DOM owns the state.

A controlled input works the other way around. React owns the state with useState, and the input’s value always comes from that state. Every keystroke fires an onChange handler that updates the state, and that re-renders the input with the new value. Once React owns the value, you can validate on every keystroke, format the input as the user types, or reset the fields after submission.

The pattern looks like this:

const [name, setName] = useState("");

<input value={name} onChange={(e) => setName(e.target.value)} />;

The value prop makes the input controlled. React is now the single source of truth for this input’s value.

Create the Add Kudo form

Let’s create a new component for the form. Create src/components/add-kudo-form.tsx:

import { useState, type FormEvent } from "react";
import type { Kudo } from "@/data/types";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";

const COLORS = [
  "bg-blue-100",
  "bg-green-100",
  "bg-purple-100",
  "bg-yellow-100",
  "bg-pink-100",
  "bg-orange-100",
];

function AddKudoForm({ onAdd }: { onAdd: (kudo: Kudo) => void }) {
  const [author, setAuthor] = useState("");
  const [message, setMessage] = useState("");

  function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();

    if (!author.trim() || !message.trim()) return;

    const newKudo: Kudo = {
      id: crypto.randomUUID(),
      author: author.trim(),
      message: message.trim(),
      color: COLORS[Math.floor(Math.random() * COLORS.length)],
    };

    onAdd(newKudo);
    // Reset the form
    setAuthor("");
    setMessage("");
  }

  return (
    <Card className="mb-8">
      <CardContent className="pt-2">
        <form onSubmit={handleSubmit} className="space-y-4">
          <div className="space-y-2">
            <Label htmlFor="author">Your Name</Label>
            <Input
              id="author"
              placeholder="Enter your name"
              value={author}
              onChange={(e) => setAuthor(e.target.value)}
            />
          </div>
          <div className="space-y-2">
            <Label htmlFor="message">Your Message</Label>
            <Textarea
              id="message"
              placeholder="Write something nice..."
              value={message}
              onChange={(e) => setMessage(e.target.value)}
            />
          </div>
          <Button type="submit">Add Kudo</Button>
        </form>
      </CardContent>
    </Card>
  );
}

export default AddKudoForm;

Let’s walk through what makes this form “controlled”:

  • Both author and message are held in useState. The <Input> and <Textarea> each get a value and an onChange prop. React holds the form state.

  • handleSubmit calls e.preventDefault() to stop the browser’s default form submission, which would reload the page. It then checks that both fields have something in them, creates a new kudo object, and passes it to the parent through the onAdd callback. Finally it resets both fields by setting them back to empty strings.

  • Because the inputs are controlled, resetting the state clears the form right away. You do not have to clear the DOM elements yourself.

Aside: The color of the kudo card is picked at random from the COLORS list of Tailwind background classes.

Checkpoint: Commit your progress.

git add .
git commit -m "kudoboard-07: Create AddKudoForm component with controlled inputs for author and message"
git push