Het patroon
We willen alle workouts uit de backend ophalen en tonen. Daarvoor splitsen we de UI in twee componenten:
Twee componenten, twee taken
WorkoutList— haalt de array op en mapt erover.Workout— toont één workout. Krijgt data via props.
Het rootbestand App.jsx rendert alleen de WorkoutList en doet zelf geen
fetch.
Maak in frontend/src/ een nieuwe map components/ aan. Daarin maak je twee
bestanden:
frontend/src/components/
├── Workout.jsx
└── WorkoutList.jsx
Workout component
Begin met de kleinste: een component dat één workout toont. Hij krijgt zijn data via
props.
// frontend/src/components/Workout.jsx
function Workout({ workout }) {
return (
<div className="workout">
<h3>{workout.title}</h3>
<p>Reps: {workout.reps}</p>
<p>Load: {workout.load} kg</p>
</div>
);
}
export default Workout;
{ workout }— destructuring van de props.workoutis het object dat we van de parent meekrijgen.- Geen state, geen fetch — alleen weergeven.
export default— zodatWorkoutListhem kan importeren.
Klein, dom, herbruikbaar
Een component dat alleen weergeeft wat hij krijgt heet een presentational component. Makkelijk te lezen, makkelijk te testen.
WorkoutList component
Deze component is verantwoordelijk voor het ophalen van de data en het mappen naar
Workout-componenten.
// frontend/src/components/WorkoutList.jsx
import { useEffect, useState } from 'react';
import Workout from './Workout';
function WorkoutList() {
const [workouts, setWorkouts] = useState([]);
useEffect(() => {
const fetchWorkouts = async () => {
const response = await fetch('http://localhost:4000/api/workouts');
const data = await response.json();
setWorkouts(data);
};
fetchWorkouts();
}, []);
return (
<div className="workout-list">
{workouts.map(workout => (
<Workout key={workout._id} workout={workout} />
))}
</div>
);
}
export default WorkoutList;
useState([])— start met lege array zodat.map()nooit crasht.useEffect(() => {...}, [])— met lege dependency array draait dit één keer als de component op het scherm verschijnt.fetchWorkoutsstaat binnen useEffect — dat is netter dan een aparte async functie buiten de hook.setWorkouts(data)— als de data binnen is, gaat hij in state. React rendert opnieuw.key={workout._id}— verplicht bij.map()in React. Gebruik altijd het MongoDB_id.workout={workout}— we geven het hele object door als prop, niet losse velden.
Waarom geen async direct in useEffect?
useEffect(async () => ...) mag niet — useEffect verwacht óf niets óf een cleanup-functie
terug, geen Promise. Daarom maken we de async functie er binnenin en roepen die direct aan.
App.jsx aansluiten
App.jsx blijft simpel: hij importeert en rendert WorkoutList, meer niet.
// frontend/src/App.jsx
import WorkoutList from './components/WorkoutList';
function App() {
return (
<div className="App">
<h1>Workouts</h1>
<WorkoutList />
</div>
);
}
export default App;
Test het in de browser
Ga naar http://localhost:5173. Je ziet je workouts uit MongoDB onder elkaar staan.
Geen workouts? Voeg er een paar toe via Postman (POST naar /api/workouts) en refresh.
Waarom in losse componenten splitsen?
Je had alles ook in App.jsx kunnen proppen. Dat werkt — voor één scherm. Maar zodra je gaat
uitbreiden met formulieren, filters en bewerk-knoppen, wordt App.jsx één lange brei. Dan vind
je niets meer terug.
Eén component, één verantwoordelijkheid
Workoutweet hoe je één workout toont.WorkoutListweet hoe je de lijst ophaalt en weergeeft.Appweet hoe je de pagina opbouwt.
Wijzig je iets aan hoe één workout eruitziet? Dan open je alleen Workout.jsx — niet een
bestand van 400 regels.
Samenvatting
Je hebt nu:
- Een
Workoutcomponent dat één workout toont via props. - Een
WorkoutListcomponent dat de array ophaalt metuseEffect + fetchen erover mapt. - Een schone
App.jsxdie alleen de lijst rendert.
Volgende Stap
Lijst staat. Maar wat zien gebruikers terwijl de fetch onderweg is, of als de backend offline is?
Feedback geven tijdens het laden en bij fouten