Rebuild the same task tracker in React. Same output, dramatically different code.
package.json— declares dependencies (React, Vite). LikeDESCRIPTION/requirements.txt.vite.config.js— build tool configindex.html— single page with one<div id="root">where React mountssrc/main.jsx— entry point; mounts<App />into#rootsrc/App.jsx— main component (state, handlers, layout)src/TaskItem.jsx— child component for each list itemsrc/index.css— same look as Part 3
cd 04-react
npm install # downloads dependencies (one-off, ~30s)
npm run dev # starts Vite dev server on port 5173Open the URL Vite prints. In Codespaces, click "Open in Browser" on the port forwarding popup.
The dev server has hot module replacement: save any file and the browser updates without losing state. Try it.
| Vanilla JS (Part 3) | React (Part 4) | |
|---|---|---|
| You write | Step-by-step DOM mutations | "Given this state, the UI looks like this" |
| Data → UI sync | Manual (render() calls everywhere) |
Automatic |
| Components | Copy-paste HTML | Reusable functions returning JSX |
| Style | Imperative | Declarative |
JSX is HTML-flavoured JavaScript. Inside a component's return, you write markup; inside {} you write JS expressions.
<input value={input} onChange={e => setInput(e.target.value)} />Gotchas:
classNamenotclass(becauseclassis a reserved word in JS)- All event handlers are camelCase:
onClick,onChange,onKeyDown - Self-close empty tags:
<input />not<input> - Only one root element per return (wrap in
<div>or<>...</>fragment)
const [tasks, setTasks] = useState([])useState returns a pair: the current value and a setter function. The destructuring [a, b] is JS array destructuring — like c(a, b) <- list(1, 2) would be in R if it worked.
The rule: never mutate state. Always pass a new value to the setter.
// WRONG — mutates the existing array, React won't re-render
tasks.push(newTask)
// RIGHT — creates a new array
setTasks([...tasks, newTask])Same for objects:
// WRONG
task.done = !task.done
// RIGHT
setTasks(tasks.map(t => t.id === id ? { ...t, done: !t.done } : t))The ... is the spread operator — it expands an array/object inline. Think of it as immutable-by-default, the way you'd write a dplyr pipeline that returns a new tibble rather than mutating in place.
TaskItem is a function that takes props and returns JSX:
function TaskItem({ task, onToggle, onDelete }) { ... }The { task, onToggle, onDelete } destructures the props object. In App.jsx we pass them as attributes:
<TaskItem task={task} onToggle={toggleTask} onDelete={deleteTask} />Data flows down through props. Events flow up by calling callback props. That's the entire mental model.
useEffect(() => {
localStorage.setItem('tasks', JSON.stringify(tasks))
}, [tasks])Runs the function after every render where tasks changed. The array [tasks] is the dependency list:
[tasks]— runs whentaskschanges[]— runs once on mount- omitted — runs after every render (rarely what you want)
useEffect is for talking to "the outside world" — localStorage, network requests, timers, subscriptions. Stuff that isn't pure rendering.
index.html— see the empty#rootdivsrc/main.jsx— see React mount into itsrc/App.jsx— read top to bottom; trace how state flowssrc/TaskItem.jsx— see how props come in, events go out
Compare side by side with 03-vanilla-js/script.js. Same app, very different code.
- Add a "Clear completed" button. You'll need an
onClickthat callssetTasks(tasks.filter(t => !t.done)). Notice you don't have to call any render function. - Show a count:
<p>{tasks.filter(t => !t.done).length} remaining</p>. This re-runs every render automatically. - Open React DevTools (browser extension) → Components tab. Click
<App />and watch state update live as you interact. - Add an
editingstate and let the user double-click a task to edit its text.
Install the React Developer Tools browser extension. It adds two tabs to devtools (Components, Profiler) that let you inspect component state and prop flow.