Wat is State Lifting?

State lifting betekent: je tilt state van een kind-component omhoog naar de ouder, zodat meerdere componenten dezelfde data kunnen delen.

De gouden regel van React:

State woont altijd in de dichtstbijzijnde gezamenlijke ouder van alle componenten die hem nodig hebben.

Waarom is dit nodig?

State is in React standaard lokaal — alleen het component dat useState aanroept kent die waarde. Maar wat als twee broer/zus-componenten dezelfde data nodig hebben? Dan moet de data ergens wonen waar ze er allebei bij kunnen: bij hun ouder.

  • Data van parent naar child → via props (naar beneden)
  • Wijzigingen van child naar parent → via een functie als prop (naar boven)

Het Probleem: state zit op de verkeerde plek

Stel je hebt een webshop met twee componenten:

  • AddButton — knop om iets in winkelmandje te doen
  • CartIcon — toont het aantal items in het mandje

Als de AddButton zijn eigen teller bijhoudt, weet CartIcon er niks van — ze zijn broer en zus, niet ouder en kind.

// ❌ KAPOT — state zit te diep
const AddButton = () => {
  const [count, setCount] = useState(0);  // alleen hier!
  return <button onClick={() => setCount(count + 1)}>Voeg toe</button>;
};

const CartIcon = () => {
  // Hoe weet deze hoeveel items er zijn? Geen idee!
  return <span>🛒 ???</span>;
};

const App = () => (
  <>
    <CartIcon />
    <AddButton />
  </>
);

Het kernidee: data kan in React alleen naar beneden stromen via props. Broers/zussen kunnen niet rechtstreeks met elkaar praten. Dus moet de gedeelde data boven hen wonen.

Zie het in actie

Hieronder zie je dezelfde app in twee versies. Klik op "Voeg toe" en kijk wat er gebeurt:

App (parent)
geen state
🛒 CartIcon
eigen state
0
➕ AddButton
eigen state
0
Probleem: elke component heeft zijn eigen count. AddButton klikt, maar CartIcon blijft op 0 staan — ze delen niks.

De Oplossing: til state op

We verplaatsen de state van AddButton naar de gezamenlijke ouder App:

// ✅ WERKT — state in de gezamenlijke ouder
const App = () => {
  const [count, setCount] = useState(0);  // state woont hier

  return (
    <>
      <CartIcon count={count} />
      <AddButton onAdd={() => setCount(count + 1)} />
    </>
  );
};

// Kind 1: krijgt data via props (naar beneden)
const CartIcon = ({ count }) => {
  return <span>🛒 {count}</span>;
};

// Kind 2: krijgt functie via props (naar boven)
const AddButton = ({ onAdd }) => {
  return <button onClick={onAdd}>Voeg toe</button>;
};

Wat is er veranderd?

  1. useState is verhuisd van AddButton naar App
  2. App geeft count door aan CartIcon via props ↓
  3. App geeft een onAdd-functie door aan AddButton
  4. Beide kinderen werken nu met dezelfde state

Stappenplan: hoe til ik state op?

Volg deze stappen wanneer twee componenten dezelfde data nodig hebben:

  1. Zoek de gezamenlijke ouder van alle componenten die de state nodig hebben.
  2. Verplaats useState uit het kind naar die ouder.
  3. Geef de waarde naar beneden door via props aan elk kind dat hem moet lezen.
  4. Geef een functie naar beneden door (bv. onAdd, onChange) aan elk kind dat hem moet updaten.
  5. Verwijder de oude state uit het kind — het wordt nu volledig "controlled" door de ouder.

Tip — naamgeving van callback props:

  • Begin met on... als het over een event gaat: onAdd, onChange, onSelect
  • Of maak duidelijk wat je doet: updateCount, handleSelect

Compleet Voorbeeld: Temperatuur omzetter

Klassiek voorbeeld: twee inputs (Celsius en Fahrenheit) die gesynchroniseerd moeten blijven. Type je in Celsius? Dan moet Fahrenheit meteen ook updaten.

import { useState } from 'react';

// Parent — houdt de waarheid vast
const TemperatureConverter = () => {
  const [celsius, setCelsius] = useState(0);

  const fahrenheit = celsius * 9/5 + 32;

  return (
    <div>
      <TemperatureInput
        label="Celsius"
        value={celsius}
        onChange={setCelsius}
      />
      <TemperatureInput
        label="Fahrenheit"
        value={fahrenheit}
        onChange={(f) => setCelsius((f - 32) * 5/9)}
      />
    </div>
  );
};

// Child — heeft zelf GEEN state, krijgt alles via props
const TemperatureInput = ({ label, value, onChange }) => {
  return (
    <label>
      {label}:
      <input
        type="number"
        value={value}
        onChange={(e) => onChange(Number(e.target.value))}
      />
    </label>
  );
};

Let op: TemperatureInput heeft geen useState! Hij is volledig "controlled" — alles komt van buitenaf. Dit heet een controlled component.

Praktische Voorbeelden

1. Filter + Lijst

Een zoekbalk filtert een lijst. De zoekterm moet bij de ouder wonen omdat zowel input als lijst hem nodig hebben.

const ProductPage = () => {
  const [search, setSearch] = useState('');
  const products = ['Appel', 'Banaan', 'Kers', 'Druif'];

  const filtered = products.filter(p =>
    p.toLowerCase().includes(search.toLowerCase())
  );

  return (
    <>
      <SearchBar value={search} onChange={setSearch} />
      <ProductList items={filtered} />
    </>
  );
};

const SearchBar = ({ value, onChange }) => (
  <input value={value} onChange={(e) => onChange(e.target.value)} />
);

const ProductList = ({ items }) => (
  <ul>{items.map(p => <li key={p}>{p}</li>)}</ul>
);

2. Geselecteerd item

Een lijst items + detail-paneel. Welk item is geselecteerd? Dat woont bij de ouder.

const App = () => {
  const [selected, setSelected] = useState(null);

  return (
    <>
      <ItemList onSelect={setSelected} />
      <ItemDetail item={selected} />
    </>
  );
};

3. Form met meerdere stappen

Een wizard met meerdere stappen — de data van alle stappen moet bewaard blijven. Dus woont alles bij de ouder.

const Wizard = () => {
  const [data, setData] = useState({ name: '', email: '', age: '' });
  const [step, setStep] = useState(1);

  const updateField = (field, value) =>
    setData({ ...data, [field]: value });

  return (
    <>
      {step === 1 && <Step1 data={data} onChange={updateField} />}
      {step === 2 && <Step2 data={data} onChange={updateField} />}
      <button onClick={() => setStep(step + 1)}>Volgende</button>
    </>
  );
};

Valkuilen & Veelgemaakte Fouten

Fout 1 — State op twee plekken tegelijk

Als je state ophaalt naar de parent, vergeet dan niet useState uit het kind te verwijderen. Anders heb je twee aparte states die uit sync raken.

// ❌ FOUT
const Child = ({ value, onChange }) => {
  const [localValue, setLocalValue] = useState(value); // dubbel!
  // ...
};

// ✅ GOED
const Child = ({ value, onChange }) => {
  // gewoon value en onChange gebruiken
  return <input value={value} onChange={(e) => onChange(e.target.value)} />;
};

Fout 2 — Te ver omhoog tillen

Til state alleen op tot de dichtstbijzijnde gezamenlijke ouder. Niet tot in App als het echt niet nodig is — dat maakt je code onnodig complex.

Fout 3 — Props drilling tot in de kelder

Als je een prop door 4+ lagen heen moet doorgeven, zit je waarschijnlijk te ver weg met je state. Overweeg dan Context API om state globaal te delen zonder elke laag door te ploegen.

Beslisboom:

  • Heeft één component de state nodig? → useState in dat component
  • Hebben twee broer/zus componenten de state nodig? → State lifting (deze pagina)
  • Hebben veel componenten verspreid door je app de state nodig? → Context API