Lesson 1.2 · 20 min read
This is the longest post in the series so far, and that's deliberate. React is the layer you'll spend the most time reading, directing, and debugging as a design engineer, so it's worth going slow and going deep. By the end of this post you won't just recognize React code. You'll be able to open a real component, understand what it does, follow the data through it, spot what's wrong, and direct changes with precision. That's a genuine, usable skill, and we're going to build it from the ground up, assuming you've never read a line of code in your life.
If some of the early parts feel basic, that's on purpose. The foundation has to be solid or nothing above it holds. Read it all once, in order. Let's begin.
Before React makes any sense, a few tiny pieces of programming vocabulary have to click. These aren't React-specific. They're the raw grammar underneath all code, and they take about five minutes to absorb. Once you have them, everything else reads more easily.
A variable is a named box that holds a value. That's the whole idea. When code says price = 29, it's creating a box labeled price and putting the number 29 inside it. Later, anywhere the code says price, it means "whatever is in that box," which is 29. Variables can hold numbers, text, lists, almost anything. The name lets you refer to a value without repeating it. You already do this constantly in design when you name a color token "primary" and reuse it everywhere. Same instinct: a name that stands for a value.
Text values are called strings, and they go in quotes. When code says "Blue Shirt", that's a string, a piece of text. The quotes are how code knows "this is literal text, not an instruction." Numbers don't need quotes; 29 is just a number. Text does; "Blue Shirt" is a string. This quote distinction matters when you read code, because it tells you what's literal text versus what's a reference to something else.
A function is a reusable set of instructions with a name. Think of a recipe. You define it once, give it a name, and then whenever you "call" that name, all the steps run. A function can take inputs (ingredients) and produce an output (the finished dish). When code says function greet(), it's defining a recipe named greet. When code later says greet(), with the parentheses, it's running that recipe. Those parentheses are the signal: something is being called, made to run, right now.
Curly braces { } group things together, and mark where code lives. When you see { }, they're wrapping up a chunk of something into one unit, the body of a function, a set of instructions, a block that belongs together. For now, just read curly braces as "here's a bundle of stuff that goes together." Their exact meaning shifts a little by context, and we'll point out the important cases as they come.
Code runs top to bottom, mostly. Unless something tells it otherwise, code executes one line at a time, from the top down, like reading a page. This simple fact helps enormously when reading: start at the top, go down, and follow what happens in order.
That's genuinely most of the grammar you need to start reading React. Variables are named boxes. Strings are quoted text. Functions are named, callable recipes. Curly braces bundle things. Code flows downward. Hold those five, and let's build React on top of them.
Now to React itself, and we start with the concept you already own better than most engineers.
Think about how you actually work in a design tool. You build a card once, a clean product card with an image, a title, a price. Then you turn it into a component, a reusable master. Now you can drop that card anywhere, over and over, and each copy shares the same structure, but each instance can hold different content: this card shows a blue shirt, that card shows a red jacket. Same component, different content per instance. Change the master once, and every instance updates.
That instinct is the entire foundation of React. In React, an interface is broken into components: reusable, self-contained pieces of UI. A button is a component. A card is a component. A whole page is a component built out of smaller components. Just like your design components, a React component is defined once and used many times, each time potentially holding different content.
And here's the deeper thing to see: every interface you've ever designed was secretly a tree. A page contains sections, which contain cards, which contain images and text and buttons. Things inside things inside things. React makes that tree literal. Each node in the tree is a component, and the whole app is one big tree of components, with a single root at the top branching down into everything else. When you internalize that React code is just describing the component tree you'd naturally draw in a design file, the foreign language starts turning familiar. You're not learning a new way to think. You're learning how a way you already think gets written down.
Here's a real, simple component. Don't try to understand every character yet. Just let your eyes move over the shape.
function ProductCard() {
return (
<div>
<img src="shirt.jpg" />
<h2>Blue Shirt</h2>
<p>$29</p>
</div>
);
}
Now let's read it with the grammar from Part 1. The word function plus the name ProductCard: this is defining a recipe named ProductCard. The curly braces after it wrap the body, the bundle of what this recipe does. Inside, the word return means "here's what this component produces," what it hands back when used. And what it returns is that block that looks almost exactly like HTML.
That HTML-looking part deserves a name, because it's central to reading React. It's called JSX, and it's simply HTML written directly inside your component code. That's genuinely all JSX is: the familiar structure language from the last post, HTML, living right inside the JavaScript. So when you see <h2>Blue Shirt</h2> inside a React component, you already know how to read it from the HTML post: it's a heading containing the text "Blue Shirt." The <img /> is an image. The <p> is a paragraph. The <div> is a generic container, a box that holds the others, just like a frame grouping layers in your design tool.
So read the whole thing in plain language: "ProductCard is a component that produces a box containing a shirt image, a heading that says Blue Shirt, and a paragraph showing $29." You just read a React component. It described a small piece of interface, exactly the way a component in your design file would. It was never a foreign language. It was a component tree with some familiar HTML inside, written down.
One small but important note on <img src="shirt.jpg" />. The src part is called an attribute, extra information attached to an element. Here it tells the image which file to show. Elements can carry attributes that configure them: an image's source, a link's destination, a button's label. You'll see attributes everywhere, and they're just "settings on this particular element."
A component that always says "Blue Shirt" isn't very useful. The power of components, in React exactly as in your design tool, is that the same component holds different content in each instance. The mechanism React uses to pass content into a component is called props, short for properties. You already know props, because they're just the per-instance content you fill into a reusable master.
Here's the card upgraded to accept props:
function ProductCard(props) {
return (
<div>
<img src={props.image} />
<h2>{props.name}</h2>
<p>{props.price}</p>
</div>
);
}
Two things changed, and both are readable now. First, ProductCard(props) has props inside its parentheses. Remember, parentheses on a function are for inputs, the ingredients handed in. So this component now accepts an input called props, a bundle of content passed to it from outside. Second, the hardcoded text became {props.name}, {props.price}, {props.image}.
Those curly braces inside JSX are a specific, important signal. Inside JSX, curly braces mean "don't treat this as literal text, run this bit of code and drop the result in here." So {props.name} says "insert the value of the name that was passed in." The props.name part is just reaching into the props bundle and pulling out the piece labeled name. (The dot is how code reaches inside something to grab a named piece of it: props.name means "the name inside props.")
So this component now reads as: "I'm a card, and I'll display whatever image, name, and price you hand me." A blank master, waiting to be filled. And here's how content gets handed in, from a parent component using this card:
<ProductCard image="shirt.jpg" name="Blue Shirt" price="$29" />
<ProductCard image="jacket.jpg" name="Red Jacket" price="$89" />
Look at that and you can feel exactly what's happening. The same ProductCard component, used twice, each time handed different props. One instance gets the blue shirt's details, the other gets the red jacket's, and each renders correctly with its own content. This is precisely the master component with overridable text and image layers, dropped in twice with different content. Props flow into a component from whoever uses it. Once you see {props.something}, you know instantly: that's a slot being filled from outside. A huge share of all React code is exactly this, components receiving props and arranging them into UI.
Props come from outside. But a component often needs to remember something on its own, something that changes over time as the user interacts. That internal, changeable memory is called state, and it is the single most important concept in this post. State is where interfaces come alive, and also where most bugs live.
We met state back in the static-versus-dynamic post: the current condition of things, what's true right now and can change. In a component, state is data the component holds and watches. A like button remembers whether it's currently liked. A dropdown remembers whether it's open or closed. A form field remembers the text typed so far. All state: things remembered in the moment, that change as the user acts.
Here's what state looks like in code. This introduces a real piece of React machinery, so we'll go slow.
function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(true)}>
{liked ? "Liked" : "Like"}
</button>
);
}
Let's decode this line by line, because this one small component contains three of React's most important ideas.
The line const [liked, setLiked] = useState(false) is how you create a piece of state. Read it in parts. useState(false) sets up a new piece of state with a starting value of false (meaning "not liked yet"). It hands back two things: liked, the current value of the state, and setLiked, a function you call to change it. So now the component has a memory called liked that starts false, plus a way to update that memory, setLiked. (const just means "define a name," another way of making a named box.)
The word useState is our first example of a hook, and hooks are worth understanding as a category, because you'll see many of them. A hook is a special React function that gives a component a capability it wouldn't otherwise have. useState gives a component memory. You can always spot a hook by its name: it starts with the word "use." When you see useState, useEffect, useContext, or any useSomething, you're looking at a hook, a plug-in ability being added to the component. For now, useState is the one that matters most: it's how components remember.
Now the button itself: onClick={() => setLiked(true)}. The onClick is an event handler, it says "when this button is clicked, do this." And what it does is setLiked(true), call that update function to change the state to true. So: click the button, and the liked state flips to true. (The () => part is just the syntax for "here's a small instruction to run when the moment comes." You'll see () => constantly; read it as "when this happens, do the following.")
Finally, {liked ? "Liked" : "Like"}. Those curly braces mean "run this code and drop the result in," as we learned. The code inside is a compact either-or: "if liked is true, show 'Liked', otherwise show 'Like'." So the button's text reflects the current state. This little ? : structure is just a quick "if this, then that, otherwise the other," and it's everywhere in React for choosing between two things based on a condition.
Now watch the whole thing move, because this is the magic. The button starts showing "Like" because liked starts false. You click. The onClick runs setLiked(true), changing the state. And here's the crucial part: when state changes, React automatically re-renders the component to reflect the new state. You don't manually update the screen. The state changed to true, so React redraws the button, and now the either-or shows "Liked." State changed, and the UI followed on its own.
This is the beating heart of how modern interfaces work, so let me say it as plainly as possible: the screen is a reflection of the current state. Whenever state changes, the screen changes to match, automatically. You manage the state, and React manages keeping the screen in sync with it. This one idea, state drives the UI, is the thing to carry out of this whole post if you carry nothing else.
I called state the home of most bugs, and that wasn't a throwaway. It's a genuine engineering insight, and understanding it will make you unusually good at debugging interfaces, including ones an AI wrote.
Because the UI is a reflection of state, almost everything that visibly goes wrong in an app is really state being wrong. Let me show you the pattern with real examples, because once you see it you'll see it forever.
The like button shows red, but the like didn't actually save to the server. What happened? The visual state ("liked") changed, but the real-world action (telling the server) failed or was never wired up. State and reality fell out of sync. The cart shows the wrong total. Why? The state holding the total didn't update correctly when an item was added. The screen is faithfully reflecting a state that's simply wrong. A screen is stuck on a loading spinner forever. Why? There's a piece of state, probably called something like isLoading, that was set to true when the load began and was supposed to flip to false when the data arrived. It never flipped. The UI is correctly showing "still loading" for a state that got stuck.
See the through-line? In every case, the UI is doing its job perfectly, reflecting the state. The bug is in the state. So when you're staring at a misbehaving interface, the single sharpest question you can ask is: what state drives this, and is that state correct right now? This reframes debugging from "why does the screen look wrong" (vague, overwhelming) to "which piece of state is wrong, and why didn't it update correctly" (specific, findable). That question, asked well, resolves an astonishing fraction of frontend bugs, and it's a question you can now ask of any component, including whatever an AI just generated for you.
Beyond props and state, two specific patterns appear in almost every real component. Knowing them by sight means far less React will surprise you.
Rendering a list. Real interfaces are full of repeated items: a feed of posts, a list of products, a set of comments. React handles this by taking a collection of data and producing one component per item. It looks like this:
function ProductList(props) {
return (
<div>
{props.products.map((product) => (
<ProductCard name={product.name} price={product.price} />
))}
</div>
);
}
Don't let .map intimidate you. map means "for each item in this collection, produce something." So props.products.map(...) reads as "for each product in the products list, produce a ProductCard for it." If you pass in ten products, you get ten cards. This is how a feed of any length gets built from a list of data: one component per item, generated automatically. Whenever you see .map in JSX, translate it in your head to "for each of these, make one of these." That single translation unlocks a large amount of real-world React, because lists are everywhere.
Showing something conditionally. Interfaces constantly need to show one thing or another depending on the situation: show the user's name if logged in, otherwise show a login button; show a spinner while loading, otherwise show the content. You'll see this written a couple of ways:
{isLoggedIn ? <UserMenu /> : <LoginButton />}
That's the either-or from before: "if logged in, show the user menu, otherwise show the login button." You'll also see:
{isLoading && <Spinner />}
Read && here as "show this only if the thing before it is true." So this says "if loading, show the spinner." (If not loading, it shows nothing.) Between the either-or ? : and the "only if" &&, you can now read the vast majority of how React decides what to show. These two patterns, mapping over lists and conditional showing, cover an enormous slice of everyday React, and you can now recognize both on sight.
Here's where everything connects back to the very first posts in the series, and where React stops being about static examples and starts being about real, live products. Remember from Module 0: to get real data, the client sends a request to the server, which queries the database and sends back a response, usually as JSON. Now we can see how a React component actually does that.
A component that needs data from the server, say, a list of products, follows a pattern so common you'll recognize it instantly once you've seen it once:
function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch("/api/products")
.then((response) => response.json())
.then((data) => setProducts(data));
}, []);
return (
<div>
{products.map((product) => (
<ProductCard name={product.name} price={product.price} />
))}
</div>
);
}
This looks like a lot, but you already understand almost all of it. Let's read it in order.
First: const [products, setProducts] = useState([]). You know this now. It's a piece of state called products, starting as an empty list ([] means empty list). The component begins knowing about zero products, because it hasn't fetched them yet.
Then comes a new hook: useEffect. This is the second hook worth knowing well. useEffect runs code at a specific moment, most commonly when the component first appears on screen. Its job is for things that need to happen alongside rendering, and the most common one by far is fetching data. So this useEffect says "when this component first loads, go do the following." (The empty [] at the very end is what tells React "run this once, when the component first appears, and not again." You don't need to master why yet; just recognize that useEffect with an empty [] means "do this once on load.")
Inside the effect is the actual request: fetch("/api/products"). fetch is how JavaScript makes a request to a server, the exact request-response journey from post 0.1, happening right here in the code. It's asking the server, at the address /api/products, for the products. What comes back is a response, and the next lines unpack it: .then((response) => response.json()) means "when the response arrives, convert it from JSON into usable data," and .then((data) => setProducts(data)) means "then take that data and put it into our products state."
And that final step is where the whole series clicks together. setProducts(data) changes the state. And you know what happens when state changes: React re-renders. So the moment the real product data arrives from the server and lands in state, the component automatically redraws, and the .map below produces a card for each real product. The screen goes from empty to full, on its own, the instant the data comes back.
Read the whole flow as one sentence: the component starts empty, asks the server for products when it loads, and when the products arrive, it saves them to state, which makes the screen redraw with a card for each one. That is how essentially every data-driven screen you use works, a feed, a search result, a dashboard, an inbox. It starts empty, fetches, saves to state, and re-renders full. You now understand the actual mechanism behind "opening an app and seeing your stuff," all the way from the tap to the pixels, connected end to end.
Let's assemble everything into one clean mental model, the thing you carry into every component you'll ever read.
Props flow down. Parent components pass props into their children. A page passes each product's details down into each card. Data moves downward, from components that have it to smaller components that display it. See props, and you're seeing content handed down from above.
State lives inside. A component holds its own state for what it needs to remember and change over time. When state changes, the component redraws to match. State is private, local memory, and it's the engine of everything interactive.
Events flow up. When the user acts, clicks, types, drags, the component catches that event and responds, usually by changing its own state or by telling its parent something happened. User actions enter as events, which change state, which triggers a redraw.
That's the entire loop of a living interface. Content flows down as props. Each piece keeps its own state. User actions arrive as events, which change state, which triggers a redraw, which updates the screen, which invites the next action. Every React app you will ever read, from a tiny widget to an enormous product, is some elaboration of this one cycle.
So when you open a big, intimidating component file, you're never actually lost. You orient with four questions: What props is this receiving from above? What state is it keeping for itself? What events is it responding to? And does it fetch anything from a server? Answer those four, and you understand the component, no matter how large it looks, without writing a single line yourself.
Here's the full payoff. When you ask an AI to build an interface, it produces exactly what this whole post described: components, with JSX, props, state, hooks, event handlers, list rendering, and fetch calls, all woven together. To someone without this model, it's an intimidating wall of code they can only paste and pray over. To you, now, it's a readable structure, and that changes everything about what you can do.
You can verify the AI built what you actually meant. You can see that a card receives the right props, that a button holds the right state, that a click flips that state, that a screen fetches from the right place. When something's wrong, you can locate it with precision instead of describing a vague symptom: "the products state isn't getting filled, the list stays empty, so the fetch or the setProducts isn't working," which points the AI at the exact few lines instead of the whole file. You can request changes in the language the code is actually built from: "pass the user's name as a prop into the header," "add a piece of state to track whether the panel is open," "fetch the comments when the component loads and map over them below." You're no longer describing wishes. You're describing structural changes in React's own terms, which is precisely why an AI, or an engineer, or future you, can act on them cleanly and correctly.
This is the whole difference between someone who generates interfaces and someone who engineers them. Both use AI to write the code. But one can read what came back, judge whether it's right, find what's wrong, and direct the fix with precision, while the other can only accept the output and hope. Everything in this long post, components, JSX, props, state, hooks, events, lists, conditionals, fetching, the flow of data, exists to put you firmly and permanently in the first group. And you got here not by memorizing how to write code from a blank file, but by learning to read it fluently, built on a component instinct you already had before you started.
Do this before the next post
Two things, and together they'll cement everything above.
First, ask any AI to "write a simple React component for a profile card that takes a name, avatar image, and bio as props, and has a follow button that toggles between Follow and Following when clicked." Read the result and find the pieces: the props coming in (name, avatar, bio), the state (whether you're currently following), the event (the click that flips it), and the conditional that shows "Follow" or "Following" based on the state. Point at each one out loud.
Second, ask the same AI to "write a React component that fetches a list of users from an API and displays each one as a card." Read that result and trace the flow from Part 8: the empty state at the start, the useEffect that runs on load, the fetch that requests the data, the setState that saves it when it arrives, and the map that renders a card for each user. Follow it as one story, empty to fetch to state to full.
You won't catch every detail, and you don't need to. If you can look at those two components and successfully locate the props, the state, the events, the fetch, and the flow of data through them, then you can read React. Not perfectly, not yet, but truly and usefully, which is the foundation everything else in this module is built on. That's zero to ten. The rest is repetition, and it comes naturally from here.