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

  1. Eén useState per invoerveld.
  2. onChange op elk input dat de state bijwerkt.
  3. handleSubmit die fetch met POST doet 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)}
/>
Wat gebeurt hier?
  • 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;
Belangrijke regels
  • e.preventDefault() — voorkomt dat de browser de pagina herlaadt bij submit.
  • Number(reps) — input geeft altijd een string terug, ook bij type="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 workouts uit WorkoutList omhoog naar App.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;
Waarom werkt dit?
  • De key prop op WorkoutList verandert na elke toevoeging.
  • React behandelt een component met een nieuwe key als een compleet nieuwe component: hij wordt opnieuw gemount en de useEffect draait 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.

Verwijderen & Filteren →

DELETE-knop per item en een filter op een enum-veld