Het patroon
Een formulier in React werkt anders dan in vanilla HTML. Je laat React de inputwaarden bijhouden in
useState (dat heet een controlled input) en stuurt bij submit een
POST-request naar je backend.
Drie stappen
- Eén
useStateper invoerveld. onChangeop elk input dat de state bijwerkt.handleSubmitdiefetchmetPOSTdoet en de form leegmaakt.
Controlled inputs
De waarde van een input komt uit state, niet uit het DOM-element. Daarom controlled: React heeft de controle.
const [title, setTitle] = useState('');
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
value={title}— de input toont altijd wat in state staat.onChange— bij elke toetsaanslag wordt de state geüpdatet.- Bij een
setTitle('')is de input leeg — perfect om na submit de form te resetten.
AddWorkoutForm component
Maak een nieuw bestand frontend/src/components/AddWorkoutForm.jsx:
// frontend/src/components/AddWorkoutForm.jsx
import { useState } from 'react';
function AddWorkoutForm({ onWorkoutAdded }) {
const [title, setTitle] = useState('');
const [reps, setReps] = useState('');
const [load, setLoad] = useState('');
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
const newWorkout = {
title,
reps: Number(reps),
load: Number(load)
};
const response = await fetch('http://localhost:4000/api/workouts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newWorkout)
});
const data = await response.json();
if (!response.ok) {
setError(data.error);
return;
}
// Reset form
setTitle('');
setReps('');
setLoad('');
setError(null);
// Vertel parent dat er een nieuwe workout is
onWorkoutAdded(data);
};
return (
<form className="add-form" onSubmit={handleSubmit}>
<h3>Nieuwe workout</h3>
<label>Titel:</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<label>Reps:</label>
<input
type="number"
value={reps}
onChange={(e) => setReps(e.target.value)}
/>
<label>Load (kg):</label>
<input
type="number"
value={load}
onChange={(e) => setLoad(e.target.value)}
/>
<button type="submit">Toevoegen</button>
{error && <p className="error">{error}</p>}
</form>
);
}
export default AddWorkoutForm;
e.preventDefault()— voorkomt dat de browser de pagina herlaadt bij submit.Number(reps)— input geeft altijd een string terug, ook bijtype="number". Forceer het naar getal voor je backend.headers: 'Content-Type': 'application/json'— vertelt Express dat er JSON in de body zit.JSON.stringify(newWorkout)— body moet een string zijn, geen object.onWorkoutAdded(data)— callback prop. Daar leggen we hieronder uit waarom.
Lijst vernieuwen na POST
De backend krijgt nu nieuwe workouts binnen — top. Maar in je WorkoutList verschijnt nog
niets, want die heeft maar één keer gefetcht (bij mounten). Twee oplossingen:
Twee patronen
- Optie A: de form geeft via een callback de nieuwe workout door aan de parent.
- Optie B: je tilt
workoutsuitWorkoutListomhoog naarApp.jsx, zodat form en lijst dezelfde state delen.
Voor één formulier + één lijst is optie A meestal genoeg.
Optie A: callback prop
De parent (App.jsx of WorkoutList) geeft een functie mee aan de form. De form
roept die aan zodra er een nieuwe workout is.
// frontend/src/App.jsx
import { useState } from 'react';
import WorkoutList from './components/WorkoutList';
import AddWorkoutForm from './components/AddWorkoutForm';
function App() {
// Truc: een teller die we ophogen om WorkoutList te laten herfetchen
const [refreshKey, setRefreshKey] = useState(0);
const handleWorkoutAdded = () => {
setRefreshKey(prev => prev + 1);
};
return (
<div className="App">
<h1>Workouts</h1>
<AddWorkoutForm onWorkoutAdded={handleWorkoutAdded} />
<WorkoutList key={refreshKey} />
</div>
);
}
export default App;
- De
keyprop opWorkoutListverandert na elke toevoeging. - React behandelt een component met een nieuwe
keyals een compleet nieuwe component: hij wordt opnieuw gemount en deuseEffectdraait opnieuw. - Resultaat: een verse fetch, dus de nieuwe workout komt erbij.
Optie B: state omhoog tillen
Schoner als je de lijst op meerdere plekken wil aanpassen (bijv. ook bij delete en edit). Je verplaatst
workouts uit WorkoutList naar App.jsx en geeft hem als prop door.
// frontend/src/App.jsx
import { useEffect, useState } from 'react';
import WorkoutList from './components/WorkoutList';
import AddWorkoutForm from './components/AddWorkoutForm';
function App() {
const [workouts, setWorkouts] = useState([]);
useEffect(() => {
fetch('http://localhost:4000/api/workouts')
.then(res => res.json())
.then(data => setWorkouts(data));
}, []);
const addWorkout = (newWorkout) => {
setWorkouts([newWorkout, ...workouts]);
};
return (
<div className="App">
<h1>Workouts</h1>
<AddWorkoutForm onWorkoutAdded={addWorkout} />
<WorkoutList workouts={workouts} />
</div>
);
}
WorkoutList hoeft dan zelf niet meer te fetchen — hij krijgt de array gewoon binnen:
function WorkoutList({ workouts }) {
return (
<div className="workout-list">
{workouts.map(workout => (
<Workout key={workout._id} workout={workout} />
))}
</div>
);
}
Lifting state up
Twee componenten die dezelfde data nodig hebben? Tel de state op naar de gemeenschappelijke parent. Dit is een van de belangrijkste React-patronen.
Veelgemaakte fouten
1. e.preventDefault() vergeten
Pagina laadt opnieuw bij submit. Alle state weg.
2. Geen Number(...) voor numerieke velden
Mongoose verwacht een Number en je stuurt "5". Krijg je validatiefouten van
je backend.
3. Form niet leegmaken na submit
Gebruiker denkt dat hij dezelfde workout twee keer moet typen. Reset altijd alle states naar
'' bij succes.
4. response.ok niet checken
Bij een validatiefout (status 400) belandt het antwoord gewoon in data en denkt je form
dat alles goed ging. Check altijd if (!response.ok).
Volgende Stap
Toevoegen werkt. Nu de andere kant: data weer weghalen en filteren.
DELETE-knop per item en een filter op een enum-veld