No walls of theory. We build a tiny AI chat, one feature at a time. Each feature teaches one React idea, and each idea starts with the problem it solves and a real-world picture.
You write a function that returns what the screen should look like. Call it like a tag: <Bubble />. The stuff you pass in (like the text) are its props, its ingredients.
function Bubble({ text }) { // text is a prop (an ingredient) return <div className="bubble">{text}</div>; } // stamp two cookies: <Bubble text="Hi!" /> <Bubble text="How are you?" />
Now the words: that function is a component. The HTML-looking syntax is JSX (it secretly compiles to React.createElement calls). Props are the function's arguments.
let count = 0; count++, the number changes in memory but the screen never updates. Why? React already drew the screen and walked away.setCount) and React repaints the whole picture to match the new number.To make React notice a change, the value has to live in state. Changing state = "hey React, redraw me." That redraw is called a re-render (React just runs your function again). Watch the box flash each time it redraws:
const [count, setCount] = useState(0); // the whiteboard setCount(count + 1); // wipe it → React redraws
Now the words: useState is a hook. It hands you the current value and a setter. Calling the setter schedules a re-render.
You set the box's value from state, and on every keystroke (onChange) you push the new text back into state. Type below and watch state update on every letter:
<input
value={draft}
onChange={e => setDraft(e.target.value)} />
Now the words: a box wired to state is a controlled input. If instead you let the DOM keep the value and grab it later with a ref, that's uncontrolled. Controlled is the default you'll reach for.
Give each item a stable key (its own id), not its array index. Here's the actual bug, live. Type in the top box, then "Add to top":
{messages.map(m => <Bubble key={m.id} text={m.text} />)}
"Re-render" means React re-runs your function to build a fresh description of the UI. Then it diffs that against the previous description and updates only the changed DOM nodes. A parent re-render re-runs children too, unless you tell it not to. Flash = re-rendered:
Now the words: the in-memory description is the virtual DOM. The diff-and-update step is reconciliation. Keys (section 4) make that diff accurate.
Put side-effects in useEffect. It runs after the render. Return a function to clean up. The dependency array controls when it re-runs. Mount a "typing…" timer and watch the log:
useEffect(() => { const id = setInterval(tick, 1000); // start the tap return () => clearInterval(id); // cleanup: close the tap }, [chatId]); // re-run only when chatId changes
[] = run once. Leave a value out = you read a stale old value. No array at all = runs every render (often an infinite loop).Read the response as a stream and append each chunk to state. Hit send:
const reader = res.body.getReader(); while (true) { const { value, done } = await reader.read(); if (done) break; setReply(prev => prev + decode(value)); // append token → redraw }
useMemo saves a computed value; useCallback saves a function.Wrap the expensive calc in useMemo with a dependency list. It only recomputes when those inputs change. Feel it, first run is slow, cached run is instant:
React.memo child (section 5) can actually skip.A Provider at the top holds the value; any child consumes it directly. Toggle the theme up top, the deep child updates with no props threaded:
The pain it removes (passing props through layers that ignore them) has a name: prop drilling.
setX calls get tangled and buggy.You dispatch an action; one reducer function computes the next state in one place. Watch each action transition the value:
useFetch), then anyone can follow it. You extract the behavior, not just markup.Move the hooks into your own useSomething function and return what callers need. Tap to compare:
// same fetch logic copy-pasted in every component… const [data,setData]=useState(); useEffect(()=>{ fetch(url).then(r=>r.json()).then(setData); },[url]);
use.Move the shared state up to the nearest common parent, pass the value and setter down as props. Both children now read one truth:
children. That's composition.// slot any content into the reusable frame <Modal><SettingsForm/></Modal> <Modal><DeleteConfirm/></Modal>