Files
2026-06-07 10:44:05 +02:00

344 lines
8.0 KiB
Markdown

# 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 (
<form action={createPoll}>
<input name="title" required />
<button>Create</button>
</form>
);
}
```
`"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 <button disabled={pending}>{pending ? "Bezig..." : "Vote"}</button>;
}
<form action={voteAction}>
<input name="option" />
<SubmitButton />
</form>
```
### 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 (
<div className="space-y-4 p-6">
<div className="h-8 bg-gray-200 rounded animate-pulse w-1/2" />
<div className="h-32 bg-gray-200 rounded animate-pulse" />
</div>
);
}
```
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 (
<div className="p-6 bg-red-50 rounded">
<h2 className="text-red-900 font-bold">Iets ging mis</h2>
<p className="text-sm">{error.message}</p>
<button onClick={reset} className="mt-4 text-blue-600 underline">
Probeer opnieuw
</button>
</div>
);
}
```
---
## 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();
<button disabled={pending}>
{pending ? "Bezig..." : "Stemmen"}
</button>
```
### 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