7a. gyakorlat – useState és useEffect
Bevezetés
Az előző órákon megismertük a React alapjait: komponensek, JSX, props és TypeScript típusozás. A 6. gyakorlatban egy statikus HTML prototípust alakítottunk React komponensekké. Az oldalunk már strukturált — de statikus. Ha rákattintunk egy gombra, semmi nem változik.
A mai óra ezt oldja meg. Két hook-ot tanulunk meg, amelyek nélkül egyetlen valódi React alkalmazás sem létezik:
useState– hogyan tárolunk és változtatunk adatot egy komponensen belüluseEffect– hogyan kezeljük a mellékhatásokat (adatbetöltés, időzítők, eseményfigyelők)
A mai óra után képes leszel:
useState-tel állapotot létrehozni és biztonságosan módosítani- Megérteni, mikor és miért renderelődik újra egy komponens
useEffect-et megfelelő dependency array-jel írni- Cleanup függvénnyel memóriaszivárgást elkerülni
A projekt megnyitása
A mai gyakorlat a sandbox-react-handout projektet használja.
cd gyak7v2/sandbox-react-handout
npm install
npm run dev
Nyisd meg a src/App.tsx fájlt. Az alkalmazás hét kis kártyára van tördelve — minden kártya egy önálló, izolált kísérlet. A fájl tele van TODO kommentekkel: ezeket kell életre kelteni.
Tartsd nyitva a böngésző fejlesztői konzolját (F12 → Console) az egész gyakorlat alatt. A useEffect feladatoknál a konzol kimenet a tananyag szerves része.
Mi az a hook?
Mielőtt belevágunk, egy perc elmélet.
A hookokat a React 16.8-ban vezette be. Olyan speciális függvények, amelyek React-specifikus funkcionalitást adnak a funkcionális komponenseknek: állapot, mellékhatás, kontextus stb.
Két szabály, amelyeket mindig be kell tartani:
- Hookokat csak komponens felső szintjén hívj meg — nem feltétel belsejében, nem ciklusban.
- Hookokat csak React komponensből hívj meg — sima JavaScript függvényből nem.
A nevük mindig use-zal kezdődik: useState, useEffect, useParams, useNavigate. Ezeket fogod látni ma és a következő gyakorlatokon is.
useState – állapot kezelése
Az állapot fogalma
Képzeld el, hogy az oldalon van egy számláló, amelynek értéke 0. Megnyomom a „+” gombot — az értéknek 1-re kell változnia. De honnan tudja a React, hogy frissíteni kell az oldalt?
Egy közönséges JavaScript változó (let ertek = 0) nem elegendő: ha megváltoztatod, a React nem értesül róla, és az oldal nem frissül.
Erre való az állapot (state). Az állapot olyan adat, amely:
- a komponenshez tartozik,
- idővel megváltozhat,
- és ha változik, a React automatikusan újrarendereli a komponenst.
Szintaxis
const [ertek, setErtek] = useState(kezdoErtek)
| Rész | Szerepe |
|---|---|
ertek | Az aktuális értéket tárolja — ezt olvasod a JSX-ben |
setErtek | Setter függvény — ezzel módosítod az értéket |
kezdoErtek | Az érték, amivel a komponens először jelenik meg |
Ez array destructuring: a useState egy kételemű tömböt ad vissza, és mi egyszerre adjuk meg mindkét elem nevét.
Soha ne módosítsd az állapotot közvetlenül! Az ertek = 5 egyszerűen egy JavaScript értékadás — a React nem értesül a változásról, az oldal nem frissül. Mindig a setter függvényt (setErtek(5)) használd.
Az import a fájl elejére:
import { useState } from 'react'
1. feladat: Számláló
Cél: A „+” gomb növelje a számot, a „-” csökkentse.
1. lépés – Állapot létrehozása
A Szamlalo függvényen belül, a return elé:
const [count, setCount] = useState(0)
0 a kezdőérték — nulláról indul.
2. lépés – Az érték megjelenítése
A JSX-ben cseréld le a hardcoded számot a state változóra:
<p className="value">{count}</p>
A kapcsos zárójel JSX-ben JavaScript kifejezést jelent. Mentés után a böngészőben ugyanúgy 0 látszik, de most az állapotból jön — nem hardcodeolva.
3. lépés – Gombok bekötése
<button onClick={() => setCount(count - 1)}>-</button>
<button onClick={() => setCount(count + 1)}>+</button>
Az onClick egy nyílfüggvényt kap. Azért kell nyílfüggvénybe csomagolni, mert különben a setCount(count + 1) az oldal betöltésekor azonnal lefutna — nem a kattintásra várna.
Amikor a setCount-ot hívod, a React újrarendereli a Szamlalo komponenst — lényegében újra meghívja a függvényt az új count értékkel. Ezért jelenik meg a friss szám.
Kísérletezz:
- Változtasd a kezdőértéket
useState(10)-re — onnan indul a számláló - Adj hozzá egy „Reset” gombot:
onClick={() => setCount(0)}
2. feladat: Be/Ki kapcsoló
Cél: Egy gomb megmutat vagy elrejt egy szöveget. A gomb felirata tükrözze az aktuális állapotot.
1. lépés – Boolean állapot
const [visible, setVisible] = useState(false)
false = kezdetben rejtett.
2. lépés – Gomb felirata és eseménykezelő
<button onClick={() => setVisible(!visible)}>
{visible ? 'Elrejt' : 'Megmutat'}
</button>
A !visible megfordítja az értéket (toggle). A ternary operátor (? :) a két lehetséges felirat közül a megfelelőt adja vissza.
3. lépés – Feltételes megjelenítés
{visible && <p className="message">Helló! Látható vagyok!</p>}
A && operátor rövidre zárja a kiértékelést: ha a bal oldal hamis, a jobb oldal nem kerül renderelésre — a DOM-ban sem jelenik meg az elem. Ez a leggyakoribb React minta feltételes renderelésre.
Kísérletezz:
- Változtasd a kezdőértéket
true-ra — betöltéskor a szöveg már látható lesz - A
&&helyett próbálj ternary-t:{visible ? <p>Látom</p> : null}
3. feladat: Kontrollált input
Cél: Szövegmező karakterszámlálóval és beírt szöveg megjelenítésével, karakterlimittel.
Mi a különbség kontrollált és nem kontrollált input között?
| Uncontrolled | Controlled | |
|---|---|---|
| Az értéket tárolja | DOM (az input maga) | React state |
| Olvasás | ref-en keresztül | közvetlenül a state-ből |
| Validálás, limit | Nehézkes | Egyszerű |
| Ajánlott? | Ritkán | Általában igen |
1. lépés – String állapot
const [text, setText] = useState('')
2. lépés – Input bekötése
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Írj valamit..."
/>
value={text}— az input értéke mindig a state tükreonChange— minden billentyűleütéskor frissíti a state-et;e.target.valueaz aktuális szöveg
3. lépés – Karakterszámláló
<p className="hint">
{text.length} / {limit} karakter
</p>
4. lépés – Szöveg megjelenítése
{text && <p className="message">Beírtad: „{text}"</p>}
Kísérletezz:
- Ha
text.length >= limit, adj hozzá extra CSS osztályt, ami pirossá teszi a számlálót - A
limitértékét csökkentsd 10-re — hamar eléri
useEffect – mellékhatások kezelése
Mi a mellékhatás?
Egy React komponens fő feladata: adatokat kap (state, props) → JSX-et ad vissza. Ennyi. De egy valódi alkalmazásban szükség van mellékhatásokra is:
- Adatok betöltése API-ból
- Időzítők (
setTimeout,setInterval) - Konzolba írás, eseményfigyelők feliratkozása/leiratkozása
Ezek kívül esnek a tiszta „adat → JSX” folyamon. A useEffect arra való, hogy ezeket a renderelési cikluson kívül, kontrolláltan végezzük.
Szintaxis
useEffect(() => {
// mellékhatás kódja
return () => {
// cleanup függvény (opcionális)
}
}, [/* dependency array */])
A dependency array
Ez a useEffect kulcsa — ez határozza meg, mikor fusson az effect:
| Dependency array | Mikor fut? |
|---|---|
| Nincs megadva | Minden render után |
[] üres tömb | Csak egyszer, mountoláskor |
[a, b] | Ha a vagy b megváltozott az előző render óta |
Ha egy useEffect-en belül használt változó nincs a dependency array-ben, az ESLint figyelmeztetni fog. Ne hagyd figyelmen kívül — a hiányzó függőség nehezen debugolható hibákhoz vezet (a hook régi, „befagyott” értékeket lát).
Az import bővítése:
import { useState, useEffect } from 'react'
4. feladat: Minden renderkor lefut
Cél: Megfigyelni a dependency array nélküli viselkedést — és megérteni, miért kerüljük.
State-ek:
const [count, setCount] = useState(0)
const [text, setText] = useState('')
Effect — dependency array nélkül:
useEffect(() => {
console.log('Effect lefutott – count:', count, '| text:', text)
}) // nincs dependency array!
JSX:
<button onClick={() => setCount(count + 1)}>Kattints ({count})</button>
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="Gépelj..." />
Teszteld: Kattints a gombra, gépelj a mezőbe — minden interakció után új sor jelenik meg a konzolban. Az effect minden render után lefut, bármelyik state változik is.
Miért kerüljük? Ha az effect API-hívást végez, minden billentyűleütésre hálózati kérés indulna. Dependency array nélküli useEffect szinte mindig hibás kód.
5. feladat: Csak mountoláskor fut le
Cél: API-hívás szimulálása — az effect csak egyszer, a komponens megjelenésekor fut le.
Mountolás = a komponens először jelenik meg az oldalon. Ellentéte: unmountolás = a komponens eltűnik az oldalról.
State-ek:
const [adat, setAdat] = useState<string | null>(null)
const [tolt, setTolt] = useState(true)
A string | null TypeScript típus: kezdetben null (nincs adat), betöltés után string lesz.
Effect üres [] array-jel:
useEffect(() => {
const timer = setTimeout(() => {
setAdat('Betöltött adat: React 19')
setTolt(false)
}, 1500)
return () => clearTimeout(timer) // cleanup
}, []) // üres []: csak mountoláskor fut
Feltételes megjelenítés:
{tolt ? <p className="hint">Betöltés...</p> : <p className="message">{adat}</p>}
Kísérletezz:
- Változtasd
1500-at3000-re — lassabb betöltés - Vedd ki a
[]-t — mi történik? Minden render újabbsetTimeout-ot indít, ami újabb rendert okoz, ami újabb timert indít → végtelen ciklus!
6. feladat: State figyelése
Cél: Automatikusan frissíteni egy derived értéket, valahányszor egy másik state megváltozik.
State-ek:
const [count, setCount] = useState(0)
const [paros, setParos] = useState(true)
Effect [count] dependency-vel:
useEffect(() => {
setParos(count % 2 === 0)
}, [count]) // csak count változásakor fut
JSX:
<p className="value">{count}</p>
<button onClick={() => setCount(count + 1)}>Növel</button>
<p className="message">{paros ? 'Páros' : 'Páratlan'}</p>
Ebben a konkrét esetben a useEffect valójában felesleges — a paros értékét közvetlenül ki lehetne számítani a renderből: const paros = count % 2 === 0. A useEffect mellékhatásokra való, nem értékek kiszámítására. Ez a példa csupán a dependency array viselkedését mutatja be.
Kísérletezz:
- Cseréld
[]-re a deps arrayt — az effect csak mountoláskor fut, aparossoha nem frissül - Vedd ki teljesen a deps arrayt —
setParosújra rendert okoz, ami újra futtatja az effectet → végtelen ciklus! Próbáld ki, mielőtt visszarakod.
7. feladat: Cleanup – időzítő leállítása
Cél: Start/stop számlálót írni. Ez a feladat a Idozito komponensbe kerül — ennek most csak az üres váza van, a megvalósítás a te feladatod.
Mi a cleanup?
Az useEffect visszatérhet egy cleanup függvénnyel. Ez lefut:
- mielőtt az effect újra lefutna (ha a deps megváltoztak),
- amikor a komponens unmountolódik.
Ha elindítasz egy setInterval-t és nem állítod le, az a háttérben tovább fut akkor is, ha a komponens már régen eltűnt az oldalról. Ez memóriaszivárgást és kiszámíthatatlan viselkedést okoz.
State-ek:
const [masodperc, setMasodperc] = useState(0)
const [fut, setFut] = useState(false)
Effect cleanup-pal:
useEffect(() => {
if (!fut) return // ha nem fut, ne indítson intervalt
const interval = setInterval(() => {
setMasodperc((s) => s + 1) // funkcionális update!
}, 1000)
return () => clearInterval(interval) // cleanup
}, [fut])
Figyeld meg a setMasodperc((s) => s + 1) szerkezetet — ez a funkcionális update. A setInterval callback bezáródik az eredeti masodperc értékére. Ha masodperc + 1-et írnánk, a callback mindig az induláskori 0-t látná, és a számláló soha nem menne 1 fölé.
JSX:
<p className="value">{masodperc}s</p>
<div className="row">
<button onClick={() => setFut(!fut)}>
{fut ? 'Megállít' : 'Elindít'}
</button>
<button onClick={() => { setFut(false); setMasodperc(0) }}>
Visszaállít
</button>
</div>
A cleanup pontos folyamata:
fut = false→ kattintasz az „Elindít” gombrafuttrue-ra vált → React újrarenderelésuseEffectlefut →setIntervalelindul- Kattintasz a „Megállít” gombra →
futfalse-ra vált - Cleanup lefut →
clearInterval(interval)leállítja az intervalt useEffectújra lefut →if (!fut) return→ azonnal kilép
Kísérletezz:
- Töröld ki a
return () => clearInterval(interval)sort. Indítsd el, állítsd meg, indítsd újra — a számláló egyre gyorsabban nő (minden indításnál egy új interval adódik hozzá, a régiek megmaradnak) - Próbáld
(s) => s + 1helyettmasodperc + 1-re cserélni — mi változik?
Összefoglalás
useState
const [ertek, setErtek] = useState(kezdoErtek)
| Szabály | Miért? |
|---|---|
| Mindig setterrel módosítsd | Különben React nem értesül a változásról |
Funkcionális update: set(x => x + 1) | Ha az új érték az előzőtől függ (pl. interval, aszinkron) |
| Komponens felső szintjén hívd meg | Hookokat nem hívhatsz feltételek vagy ciklusok belsejében |
useEffect
| Dependency array | Mikor fut? |
|---|---|
| Nincs megadva | Minden render után — szinte mindig kerüld |
[] | Csak mountoláskor |
[a, b] | Ha a vagy b megváltozott |
return () => ... | Cleanup: következő effect előtt + unmountoláskor |
Tipikus hibák
| Jelenség | Valószínű ok | Megoldás |
|---|---|---|
| Kattintás után nem frissül az oldal | Közvetlen értékadás (ertek = 5) | Használj setter függvényt |
| Végtelen újrarenderelés | Hiányzó vagy felesleges deps | Ellenőrizd az ESLint figyelmeztetéseket |
| Interval leállíthatatlan | Hiányzó cleanup | return () => clearInterval(interval) |
| Számláló mindig 1-en áll | Bezárt régi érték a callbackben | Funkcionális update: set(s => s + 1) |
A következő gyakorlatban (7b. gyakorlat) ugyanezeket az eszközöket egy valódi webshopban fogjuk alkalmazni: React Router, prop drilling, kontrollált formok és custom hook.