Module 4 · React · Deep Dive
From poking the DOM with jQuery to describing it with state — components, props, useState, events, and lists, the way React actually wants you to think.
BasicIntermediateBuild
This is the whole lesson in one idea, so slow down here. In jQuery you command the DOM, step by step: find an element, change it, find another, change that. You own the sequence of mutations.
// jQuery — imperative: you do every step by hand $("#add").on("click", function () { const name = $("#name").val(); $("#list").append("<li>" + name + "</li>"); // poke the DOM $("#name").val(""); // poke it again });
// React — declarative: describe the UI for the current state function List({ docs }) { return <ul>{docs.map(d => <li key={d}>{d}</li>)}</ul>; } // Add a doc to state → React re-renders the list. You never call append().jQuery bridge: stop thinking "find the node and change it." Start thinking "change the data; the view follows." The view is downstream of state, always.
In 2026, Vite is the standard way to spin up a plain React app (Create React App is dead). One command scaffolds everything:
# scaffold — pick "React", then "JavaScript" (or "TypeScript" later) npm create vite@latest docchat-ui cd docchat-ui npm install npm run dev # dev server with instant hot-reload at localhost:5173
The dev server watches your files and hot-reloads the browser on save — no manual refresh. Your app starts in src/main.jsx, which mounts a root component (App) into one <div id="root"> in index.html. That single div is the only DOM you ever write by hand.
JSX is HTML-looking syntax inside JavaScript. It's not a string and not HTML — it compiles to function calls. A handful of rules trip up every newcomer:
<>...</>.className, not class. class is a reserved word in JS. Also htmlFor, not for.{expression} to interpolate. Anything in curly braces is live JavaScript — variables, calls, ternaries.<img />, <br />, <input /> — the slash is required.function Greeting() { const name = "Sam"; return ( <> {/* Fragment = one root, no extra div */} <h1 className="title">Hi {name}</h1> <img src="/logo.png" alt="logo" /> {/* self-closed */} <p>{2 + 2} docs loaded</p> {/* expression */} </> ); }jQuery bridge: comments inside JSX use
{/* ... */}, not <!-- -->. And attributes are camelCase: onClick, tabIndex.
A component is just a function that returns JSX. Its name must be PascalCase — that's how React tells your components apart from plain HTML tags. You render one by writing it like a tag: <DocCard />.
Props are the inputs — a component receives one object of them and treats it as read-only. Never mutate props; they flow down from parent to child.
// A presentational component. `doc` is a prop. function DocCard({ title, pages }) { return ( <div className="card"> <h3>{title}</h3> <p>{pages} pages</p> </div> ); } // A parent composes children, passing props in. function App() { return <DocCard title="Intro" pages={12} />; }
Composition is how you build up a UI from small parts. The special prop children holds whatever you nest between a component's tags — great for wrappers like panels and layouts:
function Panel({ children }) { return <section className="panel">{children}</section>; } <Panel><p>Anything here lands in `children`.</p></Panel>jQuery bridge: a component ≈ a reusable PHP include/partial that takes arguments — except it lives in the browser and re-renders itself when its data changes.
useState & eventsProps come from above and are read-only. State is data a component owns and can change over time. You declare it with the useState Hook:
import { useState } from "react"; function Counter() { const [count, setCount] = useState(0); // [value, setter] return <button onClick={() => setCount(count + 1)}> Clicked {count} </button>; }
useState(initial) returns a pair: the current value and a setter. The re-render model is the key idea: calling the setter doesn't just change a variable — it tells React "this state changed," and React re-runs the component function to produce fresh JSX with the new value. State change → re-render. Always.
count = count + 1 or docs.push(x) does nothing visible — React doesn't know anything changed and won't re-render. Always go through the setter, and pass a new array/object, never a mutated one: setDocs([...docs, x]).
Events are camelCase props that take a function: onClick, onChange, onSubmit. For inputs, you wire value + onChange together to make a controlled input — React state is the single source of truth for what's typed:
function NameField() { const [name, setName] = useState(""); return ( <input value={name} // state drives the input onChange={(e) => setName(e.target.value)} // input updates state /> ); }jQuery bridge: no
$("#name").val() to read the field. The value is name in state — you already have it. The input and state stay in lockstep.
To render a collection, .map it into an array of JSX elements. Each element needs a stable, unique key prop:
function DocList({ docs }) { return ( <ul> {docs.map((doc) => ( <li key={doc.id}>{doc.title}</li> ))} </ul> ); }
Conditional rendering uses plain JS inside {}. Two common forms — && for "show or nothing", a ternary for "show A or B":
{docs.length === 0 && <p>No documents yet.</p>}
{loading
? <p>Loading…</p>
: <DocList docs={docs} />}
&& zero trap
{docs.length && <List />} renders a literal 0 when the list is empty, because 0 is falsy and gets printed. Compare explicitly: {docs.length > 0 && …}.
<DocList> that shows documents, and a controlled <AddDoc> form that adds a new one to local state. This is the lift-and-state pattern every form in the app will use.
Try it before peeking. A clean, runnable version:
src/App.jsx
import { useState } from "react"; function DocList({ docs }) { if (docs.length === 0) return <p>No documents yet.</p>; return ( <ul> {docs.map((doc) => ( <li key={doc.id}>{doc.title}</li> ))} </ul> ); } function AddDoc({ onAdd }) { const [title, setTitle] = useState(""); function handleSubmit(e) { e.preventDefault(); // stop the page reload const clean = title.trim(); if (!clean) return; onAdd(clean); // hand the value up to the parent setTitle(""); // reset the controlled input } return ( <form onSubmit={handleSubmit}> <input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="New document title" /> <button type="submit">Add</button> </form> ); } export default function App() { const [docs, setDocs] = useState([ { id: 1, title: "Intro.pdf" }, { id: 2, title: "Report.pdf" }, ]); function addDoc(title) { const id = Date.now(); // quick unique id setDocs([...docs, { id, title }]); // NEW array, never push() } return ( <div> <h1>DocChat — Documents</h1> <AddDoc onAdd={addDoc} /> <DocList docs={docs} /> </div> ); }
Notice the shape: App owns the state; AddDoc is a controlled form that lifts the value up via an onAdd callback; DocList is purely presentational. State lives in the common parent so both children share it. You'll repeat this exact pattern across DocChat.
Answer from memory first — retrieval is what turns "I read it" into "I know it".
React's core model is best summed up as:
What does calling a useState setter trigger?
Inside JSX, you write a CSS class with:
Why does each mapped list item need a key?
A controlled input keeps its value in: