# Les 6 — Lesstof ## Next.js QuickPoll Compleet — voltooien **Vak:** Technical Foundations **Vorige les:** Les 5 — Next.js Basics **Volgende les:** Les 7 — Supabase Setup --- ## Inhoud 1. [Waar staan we?](#1-waar-staan-we) 2. [Server Actions — forms zonder API routes](#2-server-actions--forms-zonder-api-routes) 3. [Forms in Next.js](#3-forms-in-nextjs) 4. [revalidatePath — UI bijwerken](#4-revalidatepath--ui-bijwerken) 5. [Loading + Error states uitwerken](#5-loading--error-states-uitwerken) 6. [Middleware basics](#6-middleware-basics) 7. [Environment variables](#7-environment-variables) 8. [API routes — wanneer ze handig zijn](#8-api-routes--wanneer-ze-handig-zijn) 9. [Polish — UX details die ertoe doen](#9-polish--ux-details-die-ertoe-doen) 10. [Voorbereiden op Supabase (Les 7)](#10-voorbereiden-op-supabase-les-7) --- ## 1. Waar staan we? In Les 5 bouwden we QuickPoll Part 1: - Homepage met lijst polls + create form - Detail-pagina met voting - Alles in-memory (`lib/data.ts`) Wat nog mist: - Stemmen werkt niet écht — refresh = data weg - Form-handling is matig - Geen loading states - Geen error handling - Data verdwijnt bij server-restart Deze les: alles aanscherpen + voorbereiden voor Supabase in Les 7. --- ## 2. Server Actions — forms zonder API routes Next.js 14+ introduceerde **Server Actions** — async functies die je rechtstreeks vanuit een form aan de server kunt aanroepen. Geen API route nodig. ### Basis ```tsx // app/page.tsx async function createPoll(formData: FormData) { "use server"; const title = formData.get("title") as string; // ... insert in database } export default function Page() { return (
); } ``` `"use server"` markeert deze functie als server-side. Form posts er rechtstreeks naar. ### Voordelen - Minder boilerplate (geen API route) - Progressieve enhancement (werkt zonder JS) - Type-safe — params zijn FormData - Te combineren met `revalidatePath` voor UI updates --- ## 3. Forms in Next.js ### useFormStatus + useFormState Voor loading + error states in client components: ```tsx "use client"; import { useFormStatus } from "react-dom"; function SubmitButton() { const { pending } = useFormStatus(); return ; }
``` ### Validation Zod is de standaard: ```typescript import { z } from "zod"; const schema = z.object({ title: z.string().min(3).max(100), options: z.array(z.string()).min(2), }); async function createPoll(formData: FormData) { "use server"; const parsed = schema.safeParse({ title: formData.get("title"), options: formData.getAll("options"), }); if (!parsed.success) return { error: parsed.error.message }; // ... save } ``` --- ## 4. revalidatePath — UI bijwerken Na een mutation moet je gecachte data invalideren — anders ziet user oude versie. ```tsx import { revalidatePath } from "next/cache"; async function vote(formData: FormData) { "use server"; // ... save vote revalidatePath(`/poll/${id}`); // refresh poll page revalidatePath("/"); // refresh homepage } ``` ### Alternatieven - `revalidateTag("polls")` — invalideer alle fetches met tag "polls" - `redirect("/somewhere")` — redirect after action (impliciet revalidate) --- ## 5. Loading + Error states uitwerken ### Skeleton loaders In plaats van "Loading..." → skeleton matching de UI: ```tsx // app/poll/[id]/loading.tsx export default function Loading() { return (
); } ``` Voelt sneller dan een spinner. ### Error boundary met retry ```tsx "use client"; export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void; }) { return (

Iets ging mis

{error.message}

); } ``` --- ## 6. Middleware basics `middleware.ts` in project-root = runt voor elke request. Voor auth, redirects, headers. ### Voorbeeld — protect /admin ```tsx // middleware.ts import { NextResponse, type NextRequest } from "next/server"; export function middleware(request: NextRequest) { const isAdminPath = request.nextUrl.pathname.startsWith("/admin"); const isAuthenticated = request.cookies.has("session"); if (isAdminPath && !isAuthenticated) { return NextResponse.redirect(new URL("/login", request.url)); } } export const config = { matcher: "/admin/:path*", }; ``` In Les 9 gebruiken we dit voor Supabase Auth. --- ## 7. Environment variables ### `.env.local` ``` DATABASE_URL=postgres://... NEXT_PUBLIC_APP_URL=http://localhost:3000 ``` ### Server-only vs client-available | Prefix | Beschikbaar | Voorbeeld | |--------|-------------|-----------| | (geen prefix) | Alleen server | `DATABASE_URL` | | `NEXT_PUBLIC_` | Server + client | `NEXT_PUBLIC_API_URL` | **Belangrijke regel:** secrets (API keys, DB passwords) hebben **NOOIT** `NEXT_PUBLIC_` prefix. Anders staat het in je client JS-bundle. ### Vercel In Vercel dashboard: Settings → Environment Variables. Apart voor Production / Preview / Development. --- ## 8. API routes — wanneer ze handig zijn App Router heeft ook API routes (`route.ts`). Wanneer wel/niet? ### Server actions hebben de voorkeur Voor forms binnen je app → server actions. Minder boilerplate. ### API routes voor - Webhooks (Stripe events, GitHub events) - Third-party services die HTTP endpoints verwachten - Mobile app calls - Cron jobs ### Voorbeeld ```typescript // app/api/health/route.ts export async function GET() { return Response.json({ status: "ok", time: new Date() }); } // app/api/polls/route.ts export async function POST(request: Request) { const data = await request.json(); // ... validate, save return Response.json({ ok: true }); } ``` --- ## 9. Polish — UX details die ertoe doen ### Feedback bij submit Geen feedback = user klikt twee keer. Disable button + spinner + success-toast. ```tsx const { pending } = useFormStatus(); ``` ### Optimistic updates User klikt → UI update direct → fetch op achtergrond. Voelt instant. ```tsx import { useOptimistic } from "react"; const [optimisticVotes, addOptimisticVote] = useOptimistic( votes, (current, newVote) => [...current, newVote] ); ``` ### Empty states Geen polls? Toon een mooie empty state + CTA: "Maak je eerste poll". ### Toasts `react-hot-toast` of `sonner` (komt met shadcn/ui). Goedkope feedback. --- ## 10. Voorbereiden op Supabase (Les 7) Onze QuickPoll heeft één probleem: data is in-memory. Restart = weg. ### Wat verandert in Les 7 - `lib/data.ts` wordt `lib/supabase.ts` (echte client) - In-memory arrays → database tabellen - Server actions roepen `supabase.from("polls").insert(...)` aan ### Architectuur na Les 7 ``` app/ ├── page.tsx # Server Component — fetch polls van Supabase ├── poll/ │ └── [id]/ │ └── page.tsx # Server Component — fetch by id └── lib/ └── supabase.ts # Supabase client ``` De rest van je code (page-structuur, components, server actions) blijft hetzelfde. --- ## Bronnen - **Next.js Server Actions:** https://nextjs.org/docs/app/getting-started/updating-data - **Next.js Forms guide:** https://nextjs.org/docs/app/guides/forms - **Zod:** https://zod.dev - **Next.js caching:** https://nextjs.org/docs/app/guides/caching - **Loading UI:** https://nextjs.org/docs/app/api-reference/file-conventions/loading - **Error Handling:** https://nextjs.org/docs/app/getting-started/error-handling