344 lines
8.0 KiB
Markdown
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
|