Files
novi-lessons/Les06-NextJS-QuickPoll-Part2/Les06-Lesopdracht.md
2026-06-07 10:44:05 +02:00

4.5 KiB

Les 6 — Lesopdracht

QuickPoll Part 2 — Server Actions + polish

Duur: ~120 minuten klassikaal


Doel

Aan het einde heeft QuickPoll:

  • Server actions voor vote submission
  • Een /create pagina voor nieuwe polls
  • Loading + error states
  • Optimistic updates

Stap 1 — Server Action voor voting

app/poll/[id]/actions.ts:

"use server";
import { polls } from "@/lib/data";
import { revalidatePath } from "next/cache";

export async function vote(pollId: string, optionId: string) {
  const poll = polls.find(p => p.id === pollId);
  if (!poll) throw new Error("Poll niet gevonden");

  const option = poll.options.find(o => o.id === optionId);
  if (!option) throw new Error("Optie niet gevonden");

  option.votes += 1;
  revalidatePath(`/poll/${pollId}`);
}

Stap 2 — VoteForm met server action

Refactor components/VoteForm.tsx om vote() server action te gebruiken in plaats van useState:

"use client";
import { vote } from "@/app/poll/[id]/actions";
import { useFormStatus } from "react-dom";

function VoteButton({ option }) {
  const { pending } = useFormStatus();
  return (
    <button
      disabled={pending}
      className="w-full text-left p-3 border rounded hover:bg-gray-50 disabled:opacity-50"
    >
      {option.text} ({option.votes})
    </button>
  );
}

export function VoteForm({ poll }) {
  return (
    <div className="space-y-2">
      {poll.options.map(o => (
        <form
          key={o.id}
          action={vote.bind(null, poll.id, o.id)}
        >
          <VoteButton option={o} />
        </form>
      ))}
    </div>
  );
}

Stap 3 — /create pagina

app/create/page.tsx:

import { createPoll } from "./actions";

export default function CreatePage() {
  return (
    <main className="max-w-md mx-auto p-8">
      <h1 className="text-2xl font-bold mb-4">Nieuwe poll</h1>
      <form action={createPoll} className="space-y-4">
        <input name="title" required placeholder="Vraag" className="w-full border p-2 rounded" />
        <textarea name="options" required placeholder="Opties, één per regel" rows={4} className="w-full border p-2 rounded" />
        <button className="bg-blue-600 text-white px-4 py-2 rounded">Aanmaken</button>
      </form>
    </main>
  );
}

app/create/actions.ts:

"use server";
import { polls } from "@/lib/data";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";

export async function createPoll(formData: FormData) {
  const title = formData.get("title") as string;
  const optionsText = formData.get("options") as string;
  const options = optionsText.split("\n").map(t => t.trim()).filter(Boolean);

  if (!title || options.length < 2) {
    throw new Error("Vul titel + min 2 opties in");
  }

  const newPoll = {
    id: String(polls.length + 1),
    title,
    options: options.map((text, i) => ({
      id: String.fromCharCode(97 + i),
      text,
      votes: 0,
    })),
  };

  polls.push(newPoll);
  revalidatePath("/");
  redirect(`/poll/${newPoll.id}`);
}

Stap 4 — Loading + Error states

app/poll/[id]/loading.tsx:

export default function Loading() {
  return (
    <main className="max-w-md mx-auto p-8 space-y-4">
      <div className="h-8 bg-gray-200 rounded animate-pulse w-3/4" />
      <div className="space-y-2">
        {[1,2,3].map(i => (
          <div key={i} className="h-12 bg-gray-200 rounded animate-pulse" />
        ))}
      </div>
    </main>
  );
}

app/poll/[id]/error.tsx:

"use client";
export default function Error({ error, reset }) {
  return (
    <main className="max-w-md mx-auto p-8">
      <p className="text-red-600">Oeps: {error.message}</p>
      <button onClick={reset} className="text-blue-600 underline">Probeer opnieuw</button>
    </main>
  );
}

Stap 5 — Polish

  • Voeg op homepage een "Nieuwe poll maken" knop naar /create
  • Voeg "Terug" link op detail-page
  • Maak buttons mooier (rounded, transitions, focus rings)

Eisen

  • Voting werkt via Server Action (geen lokaal useState meer)
  • /create pagina werkt en redirect na submit
  • Loading state zichtbaar bij langzaam internet (DevTools throttling)
  • Error state zichtbaar als je expres een error throw't
  • Polish: alles voelt netjes af

Bonus

  • useOptimistic toevoegen aan VoteForm voor instant feedback
  • Toast notification na vote (gebruik sonner)
  • Animatie op vote-count change

Klaar?

Push naar GitHub. Volgende les: vervang in-memory data door echte Supabase database.