<style>
body {
margin: 0;
font-family: -apple-system, sans-serif;
background-color: #fffbe8; /* try a different color */
}
.card {
padding: 24px;
}
</style>
<div class="card">
<h1>mini chat</h1>
<p>Write something here…</p>
</div>
preview — live, from your code
background-color — the whole
card follows
<h1> and
<p> text — still just tags
<p> — nothing
updates itself, you type every line
the point: every line above was typed by a human. The rest of today: teach the screen to type them for you, from data.
const messages = [
{ id: "m1", role: "assistant", text: "Hi! This is a mock chat" },
{ id: "m2", role: "user", text: "How is React different?" },
{ id: "m3", role: "assistant", text: "We describe the screen from data" },
];
console.log(messages);
console.log(messages.length);
console.log(messages[0]);
console.log(messages[0].text);
// your turn: log the role of the last message
console — live output, reruns as you type
messages is an array of objects
— same shape you'll use in every lab today
messages[0] reads one item;
.text reads one field off it
Try it: console.log(messages[3]) —
there is no fourth message. What prints, and is that an error?
const names = ["Ada Lovelace", "Grace Hopper", "Katherine Johnson"];
for (let index = 0; index < names.length; index++) {
const name = names[index];
console.log(index + " " + name);
}
// your turn: also console.log(names.length) before the loop starts
console — live output, reruns as you type
index starts at 0, stops
before names.length — three
names means indexes 0, 1, 2
names[index] is how the loop reads
one item — same bracket notation from last class
.length
hold that thought: a
for loop is one way to visit every item —
later today, .map() does the same job, but
returns the screen instead of a console line.
The web begins as a library of pages: HTML documents sitting on a server, linked together.
Click the link: the server sends a whole new page — the browser redraws every part of it.
Browsers add two companions to HTML: CSS for the look, JavaScript for behavior.
document.querySelector("#visits")
.textContent = "3rd visit!";
Rubber Duck was here.
visits: —
The page can now change without a reload — but the script must name and command every node itself.
Mail, chat, maps, feeds: pages become software people live in.
Press send — count the commands
Five updates, every send — all remembered by hand.
const item = document.createElement("li"); // 1 create the node
item.className = "me";
item.textContent = message; // 2 fill it in
document.querySelector("#log").append(item); // 3 attach it
// The header shows a message count: append must keep it honest
// too, or the screen lies. Every spot you must remember by
// hand is pain — hold that feeling for the next slide.
// TODO: append a few messages, then make the count follow
function appendMessage(text) {
const item = document.createElement("li");
item.className = "me";
item.textContent = text;
document.querySelector("#log").append(item);
}
appendMessage("first message, by hand");
preview: mini chat (manual DOM)
Facebook open-sources React — a library on top of HTML, CSS and JS, not instead of them.
const messages = [
{ me: false, text: "Hi!" },
{ me: true, text: "So, why React?" },
{ me: false, text: "Sync, for free." },
];
The same screen, now derived from the array: change the data in one place and React works out every DOM update — even the counter.
Three messages were sent, but the counter still says 2: the code that appended the li forgot the header.
The list and the counter both read the same
messages array: change the data in one
place and every reader follows.
LTS build from nodejs.org. It ships npm,
the package manager every command below runs through.
npm create vite@latest — pick a
project name, then choose React as the framework.
cd into the new folder, then
npm install — downloads React and
every package Vite scaffolded into
node_modules/.
npm run dev — open the printed
localhost address. Save a file → the browser updates itself.
<App />, save, and watch it change
without a manual refresh. That loop — save, see it instantly — is what
every lab today runs on.
$ npm create vite@latest my-app ✔ Select a framework: › React ✔ Select a variant: › JavaScript $ cd my-app $ npm install $ npm run dev VITE ready in 320 ms ➜ Local: http://localhost:5173/
No install today? Every lab in this deck runs its own sandboxed React preview right in the browser — the same App you'll write locally.
my-app/ ├── node_modules/ ← installed packages ├── public/ │ └── vite.svg ← static files, served as-is ├── src/ │ ├── assets/ ← images, fonts │ ├── App.jsx ← the root component │ ├── App.css │ ├── main.jsx ← entry point │ └── index.css ├── index.html ← the one real HTML page ├── package.json ← scripts + dependencies └── vite.config.js
index.html is nearly empty: one
<div id="root"></div> and a
<script> tag pointing at
main.jsx
main.jsx finds that
#root div and mounts
<App /> into it — this is the
only file that touches the DOM directly
App.jsx is a component like any
other: everything you build today lives in
src/, usually one component per file
node_modules/ is generated by
npm install — never edit it, never
commit it
index.html →
main.jsx renders
<App /> →
App.jsx renders whatever you build —
every lab today is really editing one link in this chain.
<div class="card"> <img class="avatar" src="ada.png"> <p>Ada Lovelace</p> </div>
Frozen: the text can't change, you write every card by hand
const card = document.createElement("div");
card.className = "card";
const img = document.createElement("img");
img.className = "avatar";
card.append(img);
card.append(p);
Flexible but long, and you must remember every update yourself
function AvatarCard() {
return (
<div className="card">
<img className="avatar" src="ada.png" />
<p>Ada Lovelace</p>
</div>
);
}
Short, and when the data changes, the screen follows
return, and the markup gets compiled to
JavaScript before the browser runs it
Predict first: what does the JSX we write arrive at the browser as: a new HTML file, JavaScript, or the DOM directly?
function Bio() {
return (
<h1>Ada Lovelace</h1>
①<p ②class="intro">First programmer</p>
<img src="ada.png"③>
);
}
function Bio() {
return (
<>
<h1>Ada Lovelace</h1>
<p className="intro">First programmer</p>
<img src="ada.png" />
</>
);
}
<div> — or an empty <>…</> fragment
<img /> has no children · attributes are camelCase: class → className, stroke-width → strokeWidth
transform.tools/html-to-jsx
Predict: <img CLASS="x" /> —
which rule does it break, and what would you write instead?
const person = {
name: "Gregorio Y. Zara",
theme: { background: "#111", color: "#fff" },
};
function Profile() {
return (
<section ①style={person.theme}>
<h1>②{person.name}'s todos</h1>
<img
className="avatar"
③src={getImageUrl(person)}
alt={person.name}
/>
</section>
);
}
{…}
replaces the quotes — style takes a
whole object
{2 + 2},
a function call {formatDate(today)}
if and
for are not expressions — branching
gets its own slide next
<p>person.name</p>
prints the literal text — <p>{person.name}</p>
prints Gregorio Y. Zara. Rendering logic and content live in the
same place.
Predict: why does { if (done) "✔" }
break the build, and how would you write the same idea correctly?
function Profile() {
return (
<img
src="https://i.imgur.com/MK3eW3As.jpg"
alt="Katherine Johnson"
/>
);
}
function Gallery() {
return (
<section>
<h1>Amazing scientists</h1>
<Profile />
<Profile />
</section>
);
}
Profile, never
profile — a lowercase tag is read as an
HTML element, not as your function
<Profile />
three times = three avatars on screen — the markup was written once
import Gallery from "./Gallery.js";
export default function App() {
return <Gallery />;
}
import Profile from "./Profile.js";
export default function Gallery() {
return (
<section>
<Profile />
<Profile />
</section>
);
}
export default function Profile() {
return <img src="…" alt="Katherine Johnson" />;
}
export default= "this is the one
thing this file offers"
import Profile from "./Profile.js":
the path is relative to this file — then use it like any tag
Predict: two files both export a
Profile: what does
import Profile from "./B.js" pick — the one
from B, both of them, or whichever loads first?
preview: mini chat
your move: ① put the message text between
<p> and
</p> ② fix the broken tag in App so it
renders <MessageBubble /> ③ add a
second <MessageBubble />, then press
Check answers.
function Avatar({ person, size }) {
return (
<img
className="avatar"
src={getImageUrl(person)}
width={size}
/>
);
}
function Profile() {
return (
<Avatar
person={{ name: "K. Saruhashi", imageId: "YfeOqp2" }}
size={100}
/>
);
}
function Card({ children }) {
return (
<div className="card">
{children}
</div>
);
}
// whatever the caller nests inside is children
<Card>
<Avatar person={katsuko} size={100} />
</Card>
{ person, size } is a plain
function parameter: one object, unpacked.
Predict: <Avatar size="100" />
vs <Avatar size={100} /> —
what is the type of size in each, and why
does it matter for width={size}?
preview: mini chat
your move: ① catch
{ name, text } in the signature ②
render each one with curly braces instead of the hardcoded label and text ③
Check answers: three calls, three different bubbles.
function Item({ name, isPacked }) {
if (isPacked) {
return <li>{name} ✔</li>;
}
return <li>{name}</li>;
}
one return per branch — clearest when
the JSX is big
<li>
{isPacked
? name + " ✔"
: name}
</li>
an expression, so it fits inside
{…} — pick A or B inline
<li>
{name} {isPacked && "✔"}
</li>
hide / show with one symbol: false renders nothing at all
&& trap
{count && <Badge />}
with count = 0 prints a lonely
0: 0 is falsy, but it's still a number React will happily
render. Guard the comparison:
{count > 0 && <Badge />}
role === "user" ? "me" : "them"
Predict: isPacked = false — what
does the ternary render? What does the
&& version render?
const people = [
{ id: 0, name: "Ada Lovelace", field: "computing" },
{ id: 1, name: "K. Saruhashi", field: "geochemistry" },
{ id: 2, name: "Grace Hopper", field: "computing" },
];
// 1. filter: pick the rows you want
const chemists = people.filter(
(person) => person.field === "computing"
);
// 2. map: transform every row → an element
const items = chemists.map((person) => (
<li key={person.id}>{person.name}</li>
));
return <ul>{items}</ul>;
map = transform every item → a new array
of elements (the array-methods slide)
filter comes first when you only want
some rows: select, then render
Predict: the same name is in the data twice and you use the name as the key — what breaks, and why does the id fix it?
preview: mini chat
your move: ① render
{text} in the empty
<p> ② use
role for label + side (ternary, pattern in
the TODO): then press Add message and watch the map keep up.
let guest = 0;
function Cup() {
guest = guest + 1;
return <h2>Tea cup for guest #{guest}</h2>;
}
function TeaSet() {
return (
<>
<Cup />
<Cup />
</>
);
}
the output depends on when it ran, not on its input
function Cup({ guest }) {
return <h2>Tea cup for guest #{guest}</h2>;
}
function TeaSet() {
return (
<>
<Cup guest={1} />
<Cup guest={2} />
<Cup guest={3} />
</>
);
}
the count comes through props: render it twice, get the same screen
Predict: the impure TeaSet — what do
the two cups show on the first render? And after a re-render?
ChatApp is the parent of
MessageList;
MessageList
is in turn the parent of every
MessageBubble —
ChatApp is not called the parent
of MessageBubble, even though it's
above it in the tree (that's a grandparent relationship: same idea,
one edge further)
ChatApp passes values down as props; it
can also pass a callback so a child can request an update while the
parent stays the owner
Predict: a new TypingIndicator
rendered inside MessageList — is it a leaf or
top-level? And which file must import it?
preview: the assembled chatbox
your move: ①
MessageList: map
messages →
MessageBubble with a
key ②
ChatHeader: render
{children} ③ wire
ChatApp: pass the data down, header text as
children.
function App() {
let draft = ""; // a plain variable
function handleInput() {
draft = "hello"; // changed... the screen doesn't care
}
return (
<p className="preview">Typing: {draft}</p>
);
}
"":
silently reassigning a variable never asks React to re-render.
You need useState:
const [draft, setDraft] = useState("")
(current value · setter that requests an update · initial value)
Data changed (say, state was set): so React starts calculating.
It asks "given this data, what should the screen be?": components answer with JSX.
React compares against the previous screen and updates exactly the nodes that differ.
<header>{messages.length} messages</header>
<ul>
{messages.map(m => <MessageBubble key={m.id} {...m} />)}
</ul>
header and
ul read the same
messages: there is no spot you must
remember to update separately
const [draft, setDraft] = useState("");
<input
value={draft}
onChange={(event) => setDraft(event.target.value)}
/>
value={draft}: what's
displayed always comes from state
onChange: every keystroke asks for a
state update
draft you read
inside a handler is the value of that render: the setter doesn't
change the variable on the very next line
setDraft("")
preview: mini chat
your move: add the
draft state, bind
value +
onChange, add a
Clear button, and make the preview follow (
{draft || "…"}). A plain variable
changes: state re-renders.
// the parent decides, the child renders
<MessageBubble role="user" text="How?" />
function MessageBubble({ role, text }) {
return <p className="text">{text}</p>;
}
// the component remembers; nobody else can touch it
const [draft, setDraft] = useState("");
setDraft(e.target.value); // the only way in
useState)
draft and
messages were two states because they are
two lifetimes — being typed vs already sent.
Predict: a sent message's text lives in messages in
App and arrives in MessageBubble as … what? While typing, the same words
live in draft — why two homes?
<form onSubmit={handleSubmit}>
<input value={draft} onChange={...} />
<button type="submit">Send</button>
</form>
function handleSubmit(event) {
event.preventDefault(); // no page reload
// ... send the message
}
In a plain browser: submitting a form = reloading the whole page (all state lost): React still has to tell the browser "no" with preventDefault
submit on the form): write
one handler, get both paths
setMessages((previous) => [...previous, yours, reply]);
setDraft("");
// provided: unique ids for every message (even duplicates)
let nextId = messages.length + 1;
const newId = () => "m" + nextId++;
[...previous, ...] = a new array holding all the
old items + the new ones at the end: the old array is untouched, and
the mock reply appears immediately (no timers/async yet)
(previous) => ... always receives the latest value:
safer than reading messages directly (snapshot!)
preview: mini chat
your move: finish
handleSubmit: prevent, trim, stop on empty,
then
setMessages(p => [...p, yours, reply]) and
clear the draft. Button and Enter share one
onSubmit.
preview: mini chat (starts empty!)
your move: the blank log says nothing and Send looks
alive on empty input — the screen lies. Fix both with
disabled={!draft.trim()} and the
empty state.
Opening the chat → ChatRoom appears for
the first time: state is born, effects start running
Typing a message / receiving new props: the same component re-renders, it isn't born again
Closing the chat → ChatRoom is removed:
state is gone, effects must clean up
display:none) is
not
unmounting: it's still in the tree, just invisible
{open && <ChatRoom roomId={roomId} />}
// open: false → true = mount
// typing / switching roomId = update
// open: true → false = unmount
Predict first: in the next demo: does typing a message make the connection reconnect?
// setLog is stable; roomId is the dependency that changes
useEffect(() => {
const connection = createConnection(roomId, (line) =>
setLog((previous) => [...previous, line])
);
connection.connect();
return () => connection.disconnect();
}, [roomId, setLog]);
setLog stable, so only a changed
roomId restarts the connection ·
createConnection is a mock · the log
lives in App, so cleanup on unmount stays visible
demo: ChatRoom (real React)
Answers to check for: typing → a quiet update, no reconnect · switching rooms → cleanup of the old room, then setup of the new one (no unmount) · closing → unmount + cleanup of the last room
Every action re-renders the component, but the Effect only
restarts when its dependency, roomId,
changes. Quiet steps are where typing does not touch the connection.
| render | computes the screen from props/state: no side effects |
| event handler | responds to one action, like hitting send: calling an API because the user sent belongs here |
| Effect | syncs with an external system after commit, like a connection, not for "reacting to a changed value" |
Don't map hooks one-to-one onto class lifecycle methods: think in the Effect's setup/cleanup cycle instead
preview: mini chat (overflows on purpose)
your move: render may not scroll (purity), Send only
covers sends: write the useEffect with
[messages] deps. Typing must not move the
log.
preview: mini chat (overflows on purpose)
your move: write
ChatLog({ messages, logRef }) —
same
map() + key
as Lab 3, now behind its own component. Stuck? press
Show solution.
function ChatLog({ messages, logRef }) {
return (
<ul className="log" ref={logRef}>
{messages.map((m) => (
<MessageBubble key={m.id} role={m.role} text={m.text} />
))}
</ul>
);
}
one component (MessageBubble, Lab 1–2),
rendered once per row of state (Lab 5–6), re-run on every
commit (Lab 7–8) — and in Lab 9 it moved into its own component
without changing shape: a list render is where every topic today
meets.
{m.role},
{m.text} (Lab 2)
messages actually
is: a new array on every send, never edited in place (Lab 5–6)
n rows of data in →
n elements out, each with a stable
key. Everything else you learned
today — props, state, events, effects — exists to decide what goes
into that array.
Predict: delete the key prop — the
app still renders. What breaks later, and when would you actually notice?
You open the running app and a bubble shows the wrong text. You get one first move:
setDraft("hi") runs: what happens,
in order, until the pixels change?
What this lesson must leave behind: being able to say: which part renders data · which part handles user actions (event handlers) · which part syncs with the outside world (Effects) before adding AI and network complexity.
Next lesson: call a real backend from the submit handler with loading, error and retry, then layer in domain data, conversation history, and answer evaluation for the chatbot.