Uitleg — Hoe werkt data fetching?

In Next.js Server Components haal je data op met gewone fetch() en async/await. Geen useEffect of useState nodig.

💡 Vergelijk met gewone React

React (Vite) — veel boilerplate:

const [workouts, setWorkouts] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
  fetch('/api/workouts')
    .then(res => res.json())
    .then(data => {
      setWorkouts(data);
      setLoading(false);
    });
}, []);

Next.js Server Component — simpel en direct:

const res = await fetch('http://localhost:3000/api/workouts');
const workouts = await res.json();

Stap 1 — Update de overzichtspagina

Vervang de inhoud van app/workouts/page.js zodat het echte data ophaalt van de API Ên het formulier toont:

// app/workouts/page.js
import Link from 'next/link';
import AddWorkoutForm from '../components/AddWorkoutForm';

async function getWorkouts() {
  // Let op: volledige URL nodig in Server Components
  const res = await fetch('http://localhost:3000/api/workouts', {
    cache: 'no-store', // Altijd verse data ophalen
  });

  if (!res.ok) {
    throw new Error('Ophalen workouts mislukt');
  }

  return res.json();
}

export default async function WorkoutsPage() {
  const workouts = await getWorkouts();

  return (
    <main>
      <h1>Mijn Workouts</h1>

      <AddWorkoutForm />

      {workouts.length === 0 ? (
        <p>Nog geen workouts. Voeg er een toe!</p>
      ) : (
        <ul>
          {workouts.map((workout) => (
            <li key={workout._id}>
              <Link href={`/workouts/${workout._id}`}>
                <strong>{workout.title}</strong>
              </Link>
               â€” {workout.reps} reps @ {workout.load}kg
            </li>
          ))}
        </ul>
      )}
    </main>
  );
}
Let op: cache: 'no-store'
  • Vanaf Next.js 15 is no-store de standaard — fetch requests worden niet gecacht
  • Je kunt cache: 'no-store' expliciet meegeven om dit duidelijk te maken
  • Wil je juist cachen? Gebruik dan cache: 'force-cache'

Stap 2 — Maak een laadscherm aan

Maak een nieuw bestand loading.js aan in de app/workouts/ map:

// app/workouts/loading.js
export default function Loading() {
  return (
    <main>
      <h1>Mijn Workouts</h1>
      <p>Workouts laden...</p>
    </main>
  );
}

💡 Automatisch!

Next.js toont loading.js automatisch terwijl je pagina data ophaalt. Je hoeft niets extra's te doen.

Stap 3 — Update de detailpagina

Vervang de inhoud van app/workouts/[id]/page.js zodat het echte data ophaalt op basis van het ID:

💡 Wat doet notFound()?

notFound() stopt de render meteen en toont de dichtstbijzijnde not-found.js pagina. Je hoeft dus niet zelf een if (!workout) return ... in je JSX te schrijven.

// app/workouts/[id]/page.js
import { notFound } from 'next/navigation';
import Link from 'next/link';
import DeleteButton from '../../components/DeleteButton';

async function getWorkout(id) {
  const res = await fetch(`http://localhost:3000/api/workouts/${id}`, {
    cache: 'no-store',
  });

  if (res.status === 404) {
    notFound(); // Stopt de render en toont app/not-found.js
  }

  if (!res.ok) {
    throw new Error('Ophalen workout mislukt');
  }

  return res.json();
}

export default async function WorkoutDetailPage({ params }) {
  const { id } = await params;
  const workout = await getWorkout(id);

  return (
    <main>
      <Link href="/workouts">← Terug naar overzicht</Link>

      <h1>{workout.title}</h1>
      <p>Reps: {workout.reps}</p>
      <p>Gewicht: {workout.load}kg</p>
      <p>Aangemaakt: {new Date(workout.createdAt).toLocaleDateString('nl-NL')}</p>

      <DeleteButton id={workout._id} />
    </main>
  );
}

Stap 4 — Maak de verwijderknop aan

Maak een nieuw bestand app/components/DeleteButton.js aan. Dit heeft interactie nodig — het is een Client Component:

// app/components/DeleteButton.js
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';

export default function DeleteButton({ id }) {
  const [deleting, setDeleting] = useState(false);
  const router = useRouter();

  async function handleDelete() {
    setDeleting(true);

    const res = await fetch(`/api/workouts/${id}`, {
      method: 'DELETE',
    });

    if (res.ok) {
      router.push('/workouts'); // Stuur terug naar overzicht
    } else {
      setDeleting(false); // Fout — zet knop terug
    }
  }

  return (
    <button onClick={handleDelete} disabled={deleting} style={{ color: 'red' }}>
      {deleting ? 'Verwijderen...' : 'Verwijder Workout'}
    </button>
  );
}
Wat is er verbeterd?
  • deleting state — knop toont "Verwijderen..." en is uitgeschakeld terwijl het bezig is
  • router.push('/workouts') — stuurt de gebruiker direct terug naar het overzicht na verwijderen
  • Bij een fout wordt de knop weer actief zodat de gebruiker het opnieuw kan proberen

Stap 5 — Ververs de lijst na toevoegen

Na het versturen van het formulier moet de lijst ververst worden. Update AddWorkoutForm.js:

// In AddWorkoutForm.js — voeg toe aan imports
import { useRouter } from 'next/navigation';

// Voeg toe bovenaan het component (naast de andere useState regels)
const router = useRouter();

// In handleSubmit, na de succesvolle POST en het resetten van de velden:
router.refresh(); // ← ververs de server data

✅ Wat je nu hebt

Na deze stap heb je een werkende fullstack app:

  • /workouts — alle workouts ophalen en tonen
  • Formulier om een nieuwe workout toe te voegen
  • /workouts/:id — detailpagina per workout
  • Workout verwijderen en terug naar overzicht

revalidatePath

revalidatePath is de server-side manier om Next.js te vertellen dat de cache voor een bepaalde route verouderd is. Dit werkt beter dan router.refresh(), want de cache wordt aan de serverkant geleegd — niet pas als de gebruiker de pagina herlaadt.

💡 Verschil met router.refresh()

router.refresh() revalidatePath()
Draait in de browser (Client Component) Draait op de server (API route)
Herlaadt data voor de huidige gebruiker Leegt de cache voor alle gebruikers

Voeg revalidatePath toe aan je API routes, direct na een schrijfactie:

// app/api/workouts/route.js
import { NextResponse } from 'next/server';
import { revalidatePath } from 'next/cache'; // ← importeer
import connectDB from '../../../lib/mongodb';
import Workout from '../../../models/Workout';

export async function POST(request) {
  await connectDB();

  const body = await request.json();
  const { title, reps, load } = body;

  if (!title || !reps || !load) {
    return NextResponse.json({ error: 'Vul alle velden in' }, { status: 400 });
  }

  const workout = await Workout.create({ title, reps, load });

  revalidatePath('/workouts'); // ← cache legen na toevoegen

  return NextResponse.json(workout, { status: 201 });
}
// app/api/workouts/[id]/route.js — in de DELETE functie
export async function DELETE(request, { params }) {
  await connectDB();

  const { id } = await params;
  const workout = await Workout.findByIdAndDelete(id);

  if (!workout) {
    return NextResponse.json({ error: 'Workout niet gevonden' }, { status: 404 });
  }

  revalidatePath('/workouts'); // ← cache legen na verwijderen

  return NextResponse.json({ message: 'Workout verwijderd' }, { status: 200 });
}

Let op

revalidatePath kan alleen in server-side code (API routes, Server Actions). Niet in Client Components. Gebruik router.refresh() als je het vanuit een Client Component wilt doen.

Checklist

✅ Check of je hebt:

  • Workouts worden opgehaald van /api/workouts
  • Lijst zichtbaar op /workouts
  • loading.js aangemaakt
  • Detailpagina toont data van de juiste workout
  • Verwijder knop werkt en navigeert terug
  • Formulier ververst de lijst na toevoegen

Volgende Stap

App werkt! Tijd om hem mooi te maken met styling.

Auth Setup →

Voeg inloggen toe aan je Workout Tracker