413 lines
8.0 KiB
Markdown
413 lines
8.0 KiB
Markdown
# Les 7: Next.js Fundamentals 2 - API Routes & Data Fetching
|
|
|
|
---
|
|
|
|
## Hoofdstuk
|
|
**Deel 2: Technical Foundations** (Les 5-9)
|
|
|
|
## Beschrijving
|
|
Leer data fetching in Next.js: Server Components, Client Components, API routes en React Query. Bouw een volledig werkende app met data.
|
|
|
|
---
|
|
|
|
## Te Behandelen
|
|
|
|
### Server Components vs Client Components
|
|
|
|
**Server Components (default in Next.js):**
|
|
- Renderen op de server
|
|
- Geen JavaScript naar de browser
|
|
- Kunnen direct data fetchen (async/await)
|
|
- Kunnen NIET: useState, useEffect, event handlers
|
|
|
|
**Client Components:**
|
|
- Renderen in de browser
|
|
- JavaScript naar de browser
|
|
- Kunnen interactief zijn
|
|
- Markeer met `'use client'` bovenaan
|
|
|
|
---
|
|
|
|
### Wanneer Wat?
|
|
|
|
```
|
|
Server Component → Data tonen, geen interactie
|
|
Client Component → Interactie nodig (forms, buttons, state)
|
|
```
|
|
|
|
**Voorbeeld:**
|
|
```tsx
|
|
// Server Component - data ophalen
|
|
async function ProductList() {
|
|
const products = await fetchProducts() // Direct fetchen!
|
|
return <ul>{products.map(p => <li>{p.name}</li>)}</ul>
|
|
}
|
|
|
|
// Client Component - interactie
|
|
'use client'
|
|
function AddToCartButton({ productId }) {
|
|
const [added, setAdded] = useState(false)
|
|
return <button onClick={() => setAdded(true)}>Add</button>
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Data Fetching in Server Components
|
|
|
|
Simpelweg `async/await` gebruiken:
|
|
|
|
```tsx
|
|
// app/products/page.tsx
|
|
interface Product {
|
|
id: number
|
|
name: string
|
|
price: number
|
|
}
|
|
|
|
async function getProducts(): Promise<Product[]> {
|
|
const res = await fetch('https://api.example.com/products')
|
|
return res.json()
|
|
}
|
|
|
|
export default async function ProductsPage() {
|
|
const products = await getProducts()
|
|
|
|
return (
|
|
<div>
|
|
<h1>Producten</h1>
|
|
<ul>
|
|
{products.map(product => (
|
|
<li key={product.id}>
|
|
{product.name} - €{product.price}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### API Routes (Route Handlers)
|
|
|
|
Bouw je eigen API in Next.js:
|
|
|
|
**Folder structuur:**
|
|
```
|
|
app/
|
|
└── api/
|
|
└── products/
|
|
└── route.ts → /api/products
|
|
```
|
|
|
|
**GET request (`app/api/products/route.ts`):**
|
|
```typescript
|
|
import { NextResponse } from 'next/server'
|
|
|
|
const products = [
|
|
{ id: 1, name: 'Laptop', price: 999 },
|
|
{ id: 2, name: 'Phone', price: 699 },
|
|
]
|
|
|
|
export async function GET() {
|
|
return NextResponse.json(products)
|
|
}
|
|
```
|
|
|
|
**POST request:**
|
|
```typescript
|
|
export async function POST(request: Request) {
|
|
const body = await request.json()
|
|
|
|
const newProduct = {
|
|
id: Date.now(),
|
|
name: body.name,
|
|
price: body.price,
|
|
}
|
|
|
|
products.push(newProduct)
|
|
|
|
return NextResponse.json(newProduct, { status: 201 })
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 'use client' Directive
|
|
|
|
Wanneer je interactie nodig hebt:
|
|
|
|
```tsx
|
|
'use client' // MOET bovenaan!
|
|
|
|
import { useState } from 'react'
|
|
|
|
export function Counter() {
|
|
const [count, setCount] = useState(0)
|
|
|
|
return (
|
|
<button onClick={() => setCount(count + 1)}>
|
|
Count: {count}
|
|
</button>
|
|
)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### React Query (TanStack Query)
|
|
|
|
**Waarom React Query?**
|
|
- Automatische caching
|
|
- Loading en error states
|
|
- Refetching (focus, interval)
|
|
- Optimistic updates
|
|
|
|
**Installatie:**
|
|
```bash
|
|
npm install @tanstack/react-query
|
|
```
|
|
|
|
**Setup Provider (`app/providers.tsx`):**
|
|
```tsx
|
|
'use client'
|
|
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
import { useState } from 'react'
|
|
|
|
export function Providers({ children }: { children: React.ReactNode }) {
|
|
const [queryClient] = useState(() => new QueryClient())
|
|
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
{children}
|
|
</QueryClientProvider>
|
|
)
|
|
}
|
|
```
|
|
|
|
**In Layout:**
|
|
```tsx
|
|
import { Providers } from './providers'
|
|
|
|
export default function RootLayout({ children }) {
|
|
return (
|
|
<html>
|
|
<body>
|
|
<Providers>{children}</Providers>
|
|
</body>
|
|
</html>
|
|
)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### useQuery - Data Ophalen
|
|
|
|
```tsx
|
|
'use client'
|
|
|
|
import { useQuery } from '@tanstack/react-query'
|
|
|
|
interface Product {
|
|
id: number
|
|
name: string
|
|
price: number
|
|
}
|
|
|
|
async function fetchProducts(): Promise<Product[]> {
|
|
const res = await fetch('/api/products')
|
|
return res.json()
|
|
}
|
|
|
|
export function ProductList() {
|
|
const { data, isLoading, error } = useQuery({
|
|
queryKey: ['products'],
|
|
queryFn: fetchProducts,
|
|
})
|
|
|
|
if (isLoading) return <div>Laden...</div>
|
|
if (error) return <div>Error: {error.message}</div>
|
|
|
|
return (
|
|
<ul>
|
|
{data?.map(product => (
|
|
<li key={product.id}>{product.name}</li>
|
|
))}
|
|
</ul>
|
|
)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### useMutation - Data Wijzigen
|
|
|
|
```tsx
|
|
'use client'
|
|
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
|
|
interface NewProduct {
|
|
name: string
|
|
price: number
|
|
}
|
|
|
|
async function createProduct(product: NewProduct) {
|
|
const res = await fetch('/api/products', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(product),
|
|
})
|
|
return res.json()
|
|
}
|
|
|
|
export function AddProductForm() {
|
|
const queryClient = useQueryClient()
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: createProduct,
|
|
onSuccess: () => {
|
|
// Invalidate and refetch
|
|
queryClient.invalidateQueries({ queryKey: ['products'] })
|
|
},
|
|
})
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
mutation.mutate({ name: 'New Product', price: 99 })
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit}>
|
|
<button type="submit" disabled={mutation.isPending}>
|
|
{mutation.isPending ? 'Toevoegen...' : 'Voeg toe'}
|
|
</button>
|
|
</form>
|
|
)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Combineren: Server + Client
|
|
|
|
```tsx
|
|
// app/products/page.tsx (Server Component)
|
|
import { ProductList } from './product-list'
|
|
import { AddProductForm } from './add-product-form'
|
|
|
|
export default function ProductsPage() {
|
|
return (
|
|
<div>
|
|
<h1>Producten</h1>
|
|
<AddProductForm /> {/* Client Component */}
|
|
<ProductList /> {/* Client Component */}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Tools
|
|
- Next.js 14
|
|
- React Query (TanStack Query)
|
|
- TypeScript
|
|
- OpenCode/WebStorm
|
|
|
|
---
|
|
|
|
## Lesopdracht (2 uur)
|
|
|
|
### Bouw CRUD App met API Routes
|
|
|
|
**Deel 1: API Routes (40 min)**
|
|
|
|
1. Maak `app/api/products/route.ts`
|
|
2. Implementeer GET (alle producten)
|
|
3. Implementeer POST (product toevoegen)
|
|
4. Test met browser/Postman: `/api/products`
|
|
|
|
**Deel 2: React Query Setup (20 min)**
|
|
|
|
1. Installeer `@tanstack/react-query`
|
|
2. Maak `app/providers.tsx`
|
|
3. Wrap app in `QueryClientProvider`
|
|
|
|
**Deel 3: Data Tonen met useQuery (30 min)**
|
|
|
|
1. Maak `ProductList` Client Component
|
|
2. Gebruik `useQuery` om data te fetchen
|
|
3. Toon loading state
|
|
4. Toon error state
|
|
5. Render product lijst
|
|
|
|
**Deel 4: Data Toevoegen met useMutation (30 min)**
|
|
|
|
1. Maak `AddProductForm` Client Component
|
|
2. Gebruik `useMutation` voor POST
|
|
3. Invalidate query na success
|
|
4. Toon "Adding..." state
|
|
|
|
### Deliverable
|
|
- Werkende API routes (GET, POST)
|
|
- ProductList met useQuery
|
|
- AddProductForm met useMutation
|
|
- Loading en error states
|
|
|
|
---
|
|
|
|
## Huiswerk (2 uur)
|
|
|
|
### Volledige CRUD Interface
|
|
|
|
**Deel 1: PUT en DELETE Routes (45 min)**
|
|
|
|
1. Maak `app/api/products/[id]/route.ts`
|
|
2. Implementeer PUT (update product)
|
|
3. Implementeer DELETE (verwijder product)
|
|
4. Test beide endpoints
|
|
|
|
**Deel 2: Update Functionaliteit (45 min)**
|
|
|
|
1. Maak edit form in ProductList
|
|
2. Gebruik useMutation voor PUT
|
|
3. Inline editing OF modal
|
|
4. Invalidate query na success
|
|
|
|
**Deel 3: Delete Functionaliteit (30 min)**
|
|
|
|
1. Voeg delete button toe per product
|
|
2. Gebruik useMutation voor DELETE
|
|
3. Voeg confirmation dialog toe
|
|
4. Invalidate query na success
|
|
|
|
**Bonus:** Optimistic Updates
|
|
- Product direct uit UI verwijderen
|
|
- Rollback als server faalt
|
|
|
|
### Deliverable
|
|
- Complete CRUD API (GET, POST, PUT, DELETE)
|
|
- UI voor alle operaties
|
|
- Error handling
|
|
- Optimistic updates (bonus)
|
|
|
|
---
|
|
|
|
## Leerdoelen
|
|
Na deze les kan de student:
|
|
- Uitleggen wanneer Server vs Client Components
|
|
- De 'use client' directive correct gebruiken
|
|
- Data fetchen in Server Components met async/await
|
|
- API routes maken met Route Handlers
|
|
- GET en POST requests implementeren
|
|
- React Query installeren en configureren
|
|
- useQuery gebruiken voor data fetching
|
|
- useMutation gebruiken voor data mutations
|
|
- Loading en error states afhandelen
|
|
- Query invalidation toepassen
|