final lessons
This commit is contained in:
343
Les06-NextJS-QuickPoll-Part2/Les06-Lesstof.md
Normal file
343
Les06-NextJS-QuickPoll-Part2/Les06-Lesstof.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user