You already invented React's core idea by hand. Every function in your Library ends with
saveBooks(); renderBooks(books); β change the data, then remember to repaint. React's
deal: you change the data, IT repaints β automatically, everywhere, always.
renderBooks(list) IS a function from data to UI β pass an array, get a table.
React keeps that and removes the other half of your burden: the spine rule "every function that
touches books must end with save β re-render" (learned painfully in Task 3.2, extended in the
capstone) disappears. You update state; React notices and re-renders every affected piece.
The bug class you've fought since Phase 2 β data changed, screen stale β is engineered away.
npm create vite@latest library-react -- --template react-ts
cd library-react
npm install
npm run dev # β http://localhost:5173, hot-reloads on every save
tsc β refresh (drilled the hard way). Vite runs that loop for
you: save the file, the browser updates itself in under a second, TS errors appear in the terminal
AND the browser. Two servers now run during dev: Vite (5173, serves the frontend) and Fastify
(3000, serves the data) β the split from Node notes Β§9, now permanent.
A component is a function whose return value is markup. Your app becomes a tree of them:
function LowStockBadge() {
return <span className="low-stock">Low stock!</span>;
}
// used like a custom HTML tag:
<LowStockBadge />
The Library's component tree β notice it's just your page, named:
<App>
ββ <SearchBox />
ββ <BookTable> β your renderBooks
β ββ <BookRow /> Γ N
ββ <AddBookForm /> β your addBook inputs
ββ <LoanTable> β your renderLoans
ββ <LoanRow /> Γ N
That HTML-in-JS is called JSX. You've been building HTML inside JS since your first render β with strings. JSX drops the quotes:
// Phase 2 you (string β browser sees it only after innerHTML):
rowsHTML += `<tr><td>${book.title}</td></tr>`;
// React you (JSX β real elements, type-checked):
<tr><td>{book.title}</td></tr>
| Your habit | JSX version | Why different |
|---|---|---|
${expr} in template literal | {expr} | JSX is not a string β no backticks, no $ |
class="low-stock" | className="low-stock" | class is a reserved JS word |
onclick='borrowBook(3)' (string) | onClick={() => borrow(3)} (function) | real function reference β typo'd names become compile errors, not silent dead buttons |
| ternary for show/hide | {loan.returned ? "Returned" : <button>...</button>} | same trick, and it can return elements |
interface BookRowProps { book: Book; onBorrow: (id: number) => void; }
function BookRow({ book, onBorrow }: BookRowProps) {
return (
<tr className={book.copies <= 2 ? "low-stock" : ""}>
<td>{book.title}</td><td>{book.copies}</td>
<td><button onClick={() => onBorrow(book.id)}>Borrow</button></td>
</tr>
);
}
book) and passes a function to call when something happens
(onBorrow). The row never touches the books array itself β it reports "id 3 was
clicked" upward, and the owner of the data acts. Your data-vs-presentation line again: rows present,
the owner decides. Interfaces for props = Phase 3 skills, directly reused (this is "React typing"
from your gap list).
books array goesimport { useState } from "react";
function App() {
const [books, setBooks] = useState<Book[]>([]);
function borrow(id: number) {
setBooks(books.map(b => b.id === id && b.copies > 0
? { ...b, copies: b.copies - 1 }
: b));
// no saveBooks(), no renderBooks() β the set triggers the repaint
}
// ...
}
book.copies = book.copies - 1, push,
splice, sort β you even learned which methods mutate (JS notes, Task 3.2).
React inverts it: state is read-only; to change it, build a NEW array/object and
hand it to setBooks. Why: React detects change by comparing old vs new β mutate the
old one and old === new, React sees "no change", screen stays stale. That's why the borrow above
uses map + spread ({ ...b, copies: ... } = copy with one field changed)
instead of assignment. Your mutate-vs-copy method knowledge just became load-bearing.
| Old Library move | React move |
|---|---|
books.push(newBook) | setBooks([...books, newBook]) |
books.splice(index, 1) | setBooks(books.filter(b => b.id !== id)) |
book.copies -= 1 | setBooks(books.map(b => b.id === id ? { ...b, copies: b.copies - 1 } : b)) |
books.sort(...) | setBooks([...books].sort(...)) β copy first, sort the copy |
<tbody>
{books.map(book => (
<BookRow key={book.id} book={book} onBorrow={borrow} />
))}
</tbody>
key β and why it's your id, again
On re-render React diffs old list vs new to update only what changed. key tells it
which row is which β identity across renders. Sound familiar? It's Task 3.5: positions
lie after sorting/deleting, ids don't. Using the array index as key is the same trap as
position-based delete β and the same fix: key={book.id}.
const [title, setTitle] = useState("");
<input value={title} onChange={e => setTitle(e.target.value)} />
<button onClick={handleAdd}>Add book</button>
getElementById(...).value (plus the as HTMLInputElement dance). React
you: state holds the value; the input just displays it and reports keystrokes. Benefits arrive
fast: live validation while typing, clearing the form = setTitle(""), and no DOM
digging at all. Your validation logic (trim/empty, Number() + isNaN)
transfers unchanged β it just reads state instead of the DOM. Strings still cross the border:
e.target.value is ALWAYS a string. Old enemy, same cure.
import { useEffect, useState } from "react";
function App() {
const [books, setBooks] = useState<Book[]>([]);
useEffect(() => {
fetch("http://localhost:3000/books")
.then(res => res.json())
.then((data: Book[]) => setBooks(data));
}, []); // [] = run once, when the component first appears
// ...
}
useEffect(fn, [])
fences it: "this touches the outside world (network) β run it once, not per render." The empty
array lists what the effect depends on; empty = nothing = run only on arrival.
const [loading, setLoading] = ... and
show a spinner or an error message. Your Library will too β users on slow networks see the loading
state far more often than developers on localhost do.
| Step | What gets built |
|---|---|
| 1. Vite project runs | hello-world component, meet hot reload |
| 2. Static BookTable | hardcoded array β table via map + key |
| 3. State + Borrow | useState, immutable updates, the no-mutation drill |
| 4. AddBookForm + search | controlled inputs, validation on state |
| 5. Fetch from Fastify | useEffect; the API becomes the source of truth |
| 6. Loans + fines UI | capstone features return, componentized |
| 7. Milestone | full parity with the Phase 2/3 app β same features, new engine |
| Idea | One line to remember |
|---|---|
| UI = f(state) | describe the screen for any data; React repaints when data changes |
| Component | a function returning UI β your renderBooks, named and nested |
| JSX | template literals without quotes; {expr}, className, onClick={fn} |
| Props | data flows down, events flow up β rows present, owners decide |
| useState | the books array's new home; change it ONLY via the setter |
| No mutation | build new arrays (map/filter/spread) β push/splice/sort-in-place are retired |
| key | identity across renders = your book.id lesson, again |
| useEffect | outside-world work (fetch), fenced so it doesn't loop |
β Prev: SQL & Prisma Β· Back to contents Β· Next: Tailwind + Zustand β