Lista de sarcini — aplicația React construită serios
Aplicația pe care o construiești azi e testul de angajare neoficial al frontend-ului: lista de sarcini. Pare banală și tocmai de-asta e perfectă — conține, în miniatură, tot ce face o aplicație reală: date în state, un formular controlat, o listă desenată din date și acțiuni care o modifică. La finalul laboratorului vei avea un to-do funcțional cap-coadă: adaugi sarcini dintr-un input, le bifezi ca terminate, le ștergi, iar contorul de „rămase" se ține singur la zi.
Ce îți trebuie
- Proiectul
prima-appdin laboratorul 5, cu serverul pornit (npm run dev). Poți lucra direct în el sau generezi unul curat cu aceeași comandănpm create vite@latest— 5 minute. - Node.js + VS Code, deja instalate.
- Cont: niciunul. Timp: e laboratorul cel mai dens din serie — 30 minute pe listă și map, 30 pe input și adăugare, 30 pe bifat/șters și misiune. Nu te grăbi la început ca să te blochezi la final; punctele de control sunt gardieni, nu decor.
Lista trăiește în state, ecranul e doar oglinda
Principiul întregului laborator, o singură frază: datele stau într-o listă în state; ecranul e desenat din listă; orice acțiune modifică lista, niciodată ecranul direct. Dacă la finalul orei poți explica fraza asta cu propriul cod, ai câștigat.
- Golește
App.jsxși pornește de la date:
import { useState } from 'react'
function App() {
const [sarcini, setSarcini] = useState([
{ id: 1, text: "Tema la mate", gata: false },
{ id: 2, text: "De adus echipamentul", gata: true },
{ id: 3, text: "Proiectul de grup: schita piesei", gata: false },
])
return (
<div>
<h1>De facut</h1>
<ul>
{sarcini.map((s) => (
<li key={s.id}>{s.text}</li>
))}
</ul>
</div>
)
}
export default App
Piesa nouă și centrală: sarcini.map(...). Metoda map ia lista și produce câte un element de pagină pentru fiecare obiect din ea — trei sarcini în state, trei rânduri pe ecran. Adaugă manual un al patrulea obiect în listă și salvează: al patrulea rând apare singur. Ecranul e oglinda listei, exact cum am promis.
Fiecare sarcină e un obiect cu trei proprietăți: id (numărul unic după care o recunoaștem), text și gata. Iar key={s.id} din li e cerința lui React de a putea urmări rândurile — fără ea primești un avertisment galben în consolă și, mai târziu, comportamente ciudate la ștergere.
Input controlat: formularul cu care adaugi
„Controlat" înseamnă că valoarea inputului nu trăiește în pagină, ci tot în state — React controlează ce scrie în căsuță. Sună birocratic, dar îți dă putere: poți goli, valida sau prepopula căsuța din cod.
- Adaugă un al doilea state și formularul:
const [textNou, setTextNou] = useState("")
function adauga() {
if (textNou.trim() === "") return
const sarcina = { id: Date.now(), text: textNou, gata: false }
setSarcini([...sarcini, sarcina])
setTextNou("")
}
și în JSX, deasupra listei:
<input
value={textNou}
onChange={(e) => setTextNou(e.target.value)}
placeholder="Ce mai ai de facut?"
/>
<button onClick={adauga}>Adauga</button>
De discutat la tablă, pentru că aici e miezul:
- Cercul complet al inputului:
value={textNou}spune căsuței ce să afișeze;onChangeprinde fiecare tastă și urcă litera în state prinsetTextNou; state-ul schimbat redesenează căsuța. Tastezi — și datele circulă prin React la fiecare caracter. [...sarcini, sarcina]construiește o listă nouă: toate elementele vechi, plus cea nouă la coadă. Regula de la laboratorul 5 rămâne lege: nu modifici state-ul direct (sarcini.push(...)e capcana clasică — React nu observă modificarea), ci dai o valoare nouă prinsetSarcini.Date.now()dă un id unic ieftin: numărul de milisecunde din momentul clickului.- Prima linie din
adaugarespinge sarcinile goale — validare de un rând care scutește lista de gunoaie.
Bifat și șters: lista se transformă, nu se editează
- Bifarea — la click pe o sarcină, starea ei
gatase inversează:
function comuta(id) {
setSarcini(sarcini.map((s) =>
s.id === id ? { ...s, gata: !s.gata } : s
))
}
Din nou map, dar cu alt rol: construiește o listă nouă în care sarcina cu id-ul potrivit e înlocuită cu o copie având gata inversat, iar toate celelalte trec neschimbate. Tipar de reținut — „schimbă unul, păstrează restul".
- Ștergerea folosește sora lui
map:
function sterge(id) {
setSarcini(sarcini.filter((s) => s.id !== id))
}
filter păstrează doar elementele care trec testul — adică toate în afară de cea ștearsă.
- Leagă-le în rândul de listă, cu tăiere vizuală pentru cele terminate:
<li key={s.id}>
<span
onClick={() => comuta(s.id)}
style={{ textDecoration: s.gata ? "line-through" : "none", cursor: "pointer" }}
>
{s.text}
</span>
<button onClick={() => sterge(s.id)}>x</button>
</li>
- Cireașa: contorul care se întreține singur, pus sub titlu:
<p>{sarcini.filter((s) => !s.gata).length} ramase din {sarcini.length}</p>
Nicio funcție de „actualizează contorul" nicăieri — el e calculat din listă la fiecare desenare. Tot laboratorul, într-o linie.
Dacă nu merge
- Avertisment
Each child in a list should have a unique "key" prop: lipseștekey={s.id}pelisau două sarcini au același id (ai adăugat manual obiecte cu id identic?). - Bifezi o sarcină și se bifează alta / ștergi una și dispare alta: id-uri duplicate sau ai folosit indexul din
mapdrept key. Id-ul unic există exact pentru asta. sarcini.pushnu face nimic pe ecran: modificare directă a state-ului — React n-o vede. Înlocuiește cusetSarcini([...sarcini, noua]).Cannot read properties of undefinedlas.text: undeva ai pus în listă altceva decât obiecte cu formatul convenit (poate un string simplu de la un test vechi). Uniformizează datele.- Stilul inline dă eroare de sintaxă: în JSX,
styleprimește un obiect — de-asta sunt două rânduri de acolade înstyle={{ ... }}. Una pentru „expresie JS", una pentru obiectul propriu-zis.
Misiunea ta
Nivel de bază (obligatoriu): aplicația completă — adăugare din input controlat cu validare, bifare cu tăiere vizuală, ștergere, contor „ramase din total".
Extindere: adăugare și cu tasta Enter (evenimentul onKeyDown pe input, verifici e.key === "Enter"), plus un buton „Sterge terminatele" care curăță dintr-un foc tot ce e bifat — un singur filter bine țintit.
Provocare: trei butoane de filtrare a vederii — „Toate / Active / Terminate" — care schimbă ce se afișează fără să șteargă nimic din listă. Îți trebuie un al treilea state pentru filtrul curent și un filter aplicat doar la afișare. E arhitectura oricărei aplicații cu taburi.
Ai terminat când:
- toate cele patru operații (adaugă, bifează, șterge, numără) funcționează fără erori sau avertismente în consolă;
- explici cu aplicația deschisă fraza-principiu de la începutul laboratorului, arătând în cod lista,
map-ul și o acțiune; - sarcinile goale nu pot intra în listă pe nicio cale;
- colegul de bancă folosește aplicația 2 minute fără să găsească un comportament ciudat — el semnează recepția.