KULTUROSFERA

Lista de sarcini — aplicația React construită serios

LaboratorVS CodeNode.js90 minD4.1HTML și CSSD3.3Programare textuală

Lecția-suport: Modelăm formele

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-app din 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.

  1. 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.

  1. 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; onChange prinde fiecare tastă și urcă litera în state prin setTextNou; 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ă prin setSarcini.
  • Date.now() dă un id unic ieftin: numărul de milisecunde din momentul clickului.
  • Prima linie din adauga respinge sarcinile goale — validare de un rând care scutește lista de gunoaie.

Bifat și șters: lista se transformă, nu se editează

  1. Bifarea — la click pe o sarcină, starea ei gata se 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".

  1. Ș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ă.

  1. 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>
  1. 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ște key={s.id} pe li sau 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 map drept key. Id-ul unic există exact pentru asta.
  • sarcini.push nu face nimic pe ecran: modificare directă a state-ului — React n-o vede. Înlocuiește cu setSarcini([...sarcini, noua]).
  • Cannot read properties of undefined la s.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, style primește un obiect — de-asta sunt două rânduri de acolade în style={{ ... }}. 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.