JSX is property syntax for HTML
Your first look at JSX, the element syntax you write React components in, and how it differs from the HTML it resembles.
You write this in a React file:
<button className="btn" onClick={handleSave}>Save</button>Then you open the browser’s Elements panel and inspect the button that rendered. This is what’s actually in the page:
<button class="btn">Save</button>Look at the differences. className became class. onClick is gone entirely: there’s no onclick attribute on the element. The click handler is bound somewhere up near the React root, and the button itself looks inert. The text you typed survived, but almost nothing else came through verbatim.
That gap is what this lesson is about. JSX looks like HTML, close enough that it’s tempting to treat it as “HTML I happen to be writing inside a JavaScript file.” It isn’t. It’s JavaScript with a small element syntax added on top, and it differs from raw HTML in a handful of specific, learnable places. Each of those places is where a newcomer tends to ship a bug and an experienced engineer catches it on sight.
You already did the hard conceptual work for this back in the DOM chapter, in “Attributes vs. properties.” There you learned that the DOM exposes its elements as objects whose property names sometimes differ from the attribute names you write in HTML: className instead of class, htmlFor instead of for. The reason is that class and for are reserved words in JavaScript, so you can’t name a property with them. That lesson explained why the names differ; this lesson is where it pays off, because those same property names are what you write in JSX every day. By the end you’ll read and write JSX fluently, interpolate JavaScript into it, render lists with correct keys, and recognize each difference from HTML as a deliberate decision rather than a rule to memorize.
JSX compiles to element descriptors
Section titled “JSX compiles to element descriptors”Before any of the differences make sense, you need a single mental model of what JSX is and what happens to it. Once you hold that model, every difference later in the lesson has somewhere to attach.
A piece of JSX is not a string and not HTML. It’s a JavaScript expression that evaluates to an object. When the code ships, a build tool rewrites your JSX into plain function calls before the browser ever sees it. This rewrite is the JSX transform, and in a Next.js app it’s run by Turbopack using what React calls the automatic runtime . You never configure it, you never call it by hand, and you’ll rarely see its output. Still, it’s worth knowing it’s there, because the bugs in this lesson come from this translation step.
Here’s the rewrite. This JSX:
<div className="row">Hello</div>becomes this function call:
jsx('div', { className: 'row', children: 'Hello' });That jsx(...) call doesn’t touch the DOM. It returns a plain JavaScript object, an element descriptor , that looks roughly like { type: 'div', props: { className: 'row', children: 'Hello' } }. It’s a recipe, not the meal. Only later, when React renders, does it walk the tree of these descriptors and build the real DOM nodes the browser paints.
So there’s a chain with two invisible steps in the middle:
Two of those four stages are invisible to you: the transform and the descriptor object. You write stage one, and the browser shows you stage four. Every difference between what you wrote and what the browser got comes from a translation you never see, which is why those differences are easy to miss. Keep this picture in mind for the rest of the lesson, because each section names one specific thing that changes between stage one and stage four.
Lowercase is a tag, uppercase is a component
Section titled “Lowercase is a tag, uppercase is a component”Notice the first argument to jsx in the examples above: the string 'div', the string 'button'. The transform decides what to put there by reading one thing, the capitalization of the element name. This is a mechanical rule, not a style choice, and it trips up almost everyone once.
A name that starts lowercase compiles to a string:
<button>Save</button>// → jsx('button', { children: 'Save' })A string in that first position means “a built-in HTML element,” what React calls an intrinsic element . React knows how to turn the string 'button' into a real <button> DOM node.
A name that starts uppercase compiles to a reference to a value instead:
<SignInForm />// → jsx(SignInForm, {})Here SignInForm is your own component, a function React will call to get more descriptors. There are no quotes, because it’s a variable in scope, not a tag name.
This is where the mistake happens. If you write your component name in lowercase, the transform reads it as an HTML tag. <signInForm /> doesn’t render your component: it asks the browser for a nonexistent <signinform> element, so you get nothing on the page and no error pointing at the cause. Component names are capitalized because the capitalization is the instruction. You’ll write your own components in a later chapter; for now you only need the naming rule so the rest of this lesson’s examples read correctly.
Props are the DOM property names you already know
Section titled “Props are the DOM property names you already know”Here’s the central idea of the whole lesson, stated plainly: JSX prop names are DOM property names. React did not invent them. When you write className in JSX, you’re using the same identifier you’d use to read or set that property on a DOM node in plain JavaScript, element.className. JSX just lets you set it declaratively in markup.
This is why “Attributes vs. properties” matters so much here. You already learned the rename table, and JSX is simply where you use it. Here it is regrouped around what actually changes.
Renamed because the HTML attribute name is a reserved word or not a valid camelCase identifier. The DOM exposes these under a different name, and JSX follows the DOM:
| You write in JSX | The HTML attribute it maps to |
| --- | --- |
| className | class |
| htmlFor | for |
| tabIndex | tabindex |
| readOnly | readonly |
| maxLength | maxlength |
| colSpan | colspan |
| rowSpan | rowspan |
The pattern to follow is the DOM property’s camelCase, not the HTML attribute’s lowercase. class and for are renamed outright because they’re reserved words; the rest are just camelCased to match their DOM property.
Passed straight through, unchanged. data-* and aria-* attributes are written exactly as they appear in HTML, kebab-case and all, and they arrive in the DOM untouched:
<div data-row-id="42" aria-live="polite" />Why the inconsistency? Because these attributes have no single camelCase DOM property to mirror. data-* attributes are read back through the dataset object, not through a property named dataRowId, so there’s nothing for JSX to camelCase toward. Reading them back is its own lesson; here you just write them as-is.
Event handlers are camelCased, and the value is a function. This is the biggest departure from HTML, so slow down for it. In HTML you’d write an event handler as a string of code: <button onclick="doThing()">. In JSX, the prop is camelCase (onClick, onChange, onSubmit, onKeyDown, onFocus, onBlur) and its value is an actual function, passed by reference:
<button onClick={handleSave}>Save</button>You’re handing React the function handleSave and saying “call this when the button is clicked.” React holds onto it and invokes it later. That word, later, is the heart of a bug you’ll likely hit within your first week, so it’s worth understanding now.
Watch the parentheses:
<button onClick={handleSave}>Save</button><button onClick={handleSave()}>Save</button>The second line adds only two characters, the parentheses, but it changes the meaning completely. onClick={handleSave()} doesn’t pass handleSave; it calls it immediately, every time the component renders, and passes whatever it returns as the click handler. If handleSave saves data, you’ve just fired a save on render instead of on click. If it returns nothing, you’ve set onClick={undefined} and the button does nothing when clicked. Either way it’s wrong, and the symptom, a side effect at the wrong time or a dead button, rarely points back at the parentheses.
When you genuinely need to pass arguments, the fix is an arrow function, which defers the call:
<button onClick={() => removeRow(row.id)}>Delete</button>Now onClick holds a function again, one that calls removeRow(row.id) when invoked. React calls that on click, and removeRow runs at the right moment.
Here’s one small, realistic block that ties the rename rules together: a labeled email field with a save button.
<form> <label htmlFor="email" className="field-label"> Email </label> <input id="email" data-analytics="email-field" tabIndex={0} /> <button onClick={handleSave}>Save</button></form>htmlFor, not for. for is a reserved word in JavaScript, so the DOM, and therefore JSX, exposes it as htmlFor. It ties this label to the input with the matching id.
<form> <label htmlFor="email" className="field-label"> Email </label> <input id="email" data-analytics="email-field" tabIndex={0} /> <button onClick={handleSave}>Save</button></form>className, not class, for the same reason: class is reserved. This is the single most common JSX typo, and the one you’ll reach for most.
<form> <label htmlFor="email" className="field-label"> Email </label> <input id="email" data-analytics="email-field" tabIndex={0} /> <button onClick={handleSave}>Save</button></form>tabIndex, camelCased to match the DOM property element.tabIndex. The HTML attribute is the lowercase tabindex. Note the value is {0}, a number, not the string "0".
<form> <label htmlFor="email" className="field-label"> Email </label> <input id="email" data-analytics="email-field" tabIndex={0} /> <button onClick={handleSave}>Save</button></form>data-* attributes pass straight through, kebab-case intact. No camelCasing and no rename: written and rendered identically.
<form> <label htmlFor="email" className="field-label"> Email </label> <input id="email" data-analytics="email-field" tabIndex={0} /> <button onClick={handleSave}>Save</button></form>onClick, camelCase, and the value is the function handleSave by reference. It’s not a string of code like HTML’s onclick="…", and not a call.
TypeScript is your safety net for the rename typos. Built-in elements are typed, so class or classname on a <div> is a red squiggle at author time, before the code ever runs. We’ll come back to how near the end of the lesson. For now, trust that the most common mistake here is caught the moment you make it.
One last nuance, because it removes a real source of confusion. A number in a prop value and a number as a child look the same but do different things. <div data-count={5} /> produces the attribute data-count="5", because HTML attributes are always strings, so React stringifies the 5. But <div>{5}</div> produces a text node containing 5, the same as the text “5” you’d see on the page. The same {5} in two positions gives two outcomes.
Now check that you can tell renamed props from unchanged ones on sight, which is the recognition skill you’ll use daily.
Click every prop whose JSX name differs from its HTML attribute name, then press Check.
<label htmlFor="search" className="lbl" id="search-label"> <input type="search" tabIndex={0} data-testid="search-box" /></label>Show the answer
The three renamed props are htmlFor (the HTML attribute is for, a reserved word), className (the attribute is class, also reserved), and tabIndex (camelCased to match the DOM property element.tabIndex; the attribute is the lowercase tabindex).
The other three are decoys — written identically in HTML and JSX. id and type are already valid lowercase identifiers with no DOM-property rename, and data-* attributes (data-testid) pass straight through, kebab-case intact, because they have no camelCase property to mirror.
JavaScript lives in curly braces
Section titled “JavaScript lives in curly braces”So far the prop values have been static. The moment you need a computed value, like a URL from a variable, a disabled state from a boolean, or a user’s name from an object, you reach for curly braces. {} opens an expression slot, a window from markup into live JavaScript. Whatever you put inside is evaluated as a JavaScript expression, and the result is dropped into that spot.
There are exactly two places a {} slot can go. As a prop value:
<a href={profileUrl}>Profile</a><button disabled={isLoading}>Save</button>And as a child, between the tags:
<h1>Welcome, {user.name}</h1><p>Total: {formatCurrency(amount)}</p>Both user.name and formatCurrency(amount) are expressions: they evaluate to a value. That word, expression, marks the boundary, and it’s where newcomers from other languages get caught. A {} slot accepts an expression , never a statement . You cannot write an if block or a for loop directly inside {}, because those don’t evaluate to anything you could drop into the markup.
This isn’t a limitation to fight; it’s a choice about how you structure the component. When the logic is complex, compute it above the return in ordinary statement-land and reference the result inside the slot:
const greeting = user ? `Welcome back, ${user.name}` : 'Welcome';
return <h1>{greeting}</h1>;When the logic is small, use an inline expression form, a ternary or &&, which you’ll see in the conditional-rendering section. Either way the rule holds: statements go above, expressions go in the braces.
What can a slot actually render? This table is worth learning by heart, because two of its rows explain bugs later in this lesson:
| Value in the slot | What appears on the page |
|---|---|
string — ‘hi’ | the text |
number — 42, 0 | the text — including 0 |
null | nothing |
undefined | nothing |
false | nothing |
true | nothing |
array | each element rendered in turn |
Two rows carry weight. First, null, undefined, false, and true all render nothing: no error, no blank space, just absence. That’s not an accident; it’s the mechanism that makes conditional rendering work, as you’ll see in a moment. Second, numbers render as text, and 0 is a number, so 0 shows up on the page. Hold on to the contrast between “false renders nothing” and “0 renders,” because the gap between them is the most famous footgun in React, and you’ll meet it shortly.
The “renders nothing” rule also gives you a safety habit. Reaching into an object that turned out to be undefined throws an error: user.name crashes the render if user is undefined. Guard it with optional chaining, and the slot quietly renders nothing instead of crashing:
<h1>{user?.name}</h1>When user is absent, user?.name is undefined, and undefined renders nothing. The page stays standing.
Rendering lists with map and the key rule
Section titled “Rendering lists with map and the key rule”The array row in that table is the one you’ll lean on most, because it’s how every list in your UI gets built. A {} slot can hold an array of descriptors, and React renders each one in turn. Pair that with .map, and you’ve got the standard pattern for turning data into markup:
<ul> {rows.map((row) => ( <li key={row.id}>{row.label}</li> ))}</ul>rows.map(...) produces an array of <li> descriptors, the slot renders them in order, and you get a list. That part is straightforward. The key is the detail that takes some care, and it causes real bugs when it’s wrong, so it’s worth getting right.
Every item in a mapped list needs a key, and the key must satisfy three constraints. It must be stable, so the same item gets the same key across every render. It must be unique among its siblings, so no two items in the same list share a key. And it must be tied to the data, not to anything about the rendering, which almost always means the item’s own ID, key={row.id}.
Why does React demand this? When the data changes and the list re-renders, React has to figure out which of the new items corresponds to which of the old ones, so it can update the DOM surgically instead of rebuilding it. The key is the identity tag it uses to make that match: the same key means “this is the same item, just possibly changed.” Without a key, React falls back to matching by position, first item to first item, second to second, which is wrong the instant the list reorders. The full machinery here is reconciliation , and you’ll meet it properly when we get to React’s render model. For now you need the rule and the bug, not the algorithm.
Here’s something subtle worth knowing even though you can’t see it: key is not a normal prop. The transform pulls it out separately, so the real call shape is jsx(type, props, key), with key as its own third argument. A component never actually receives key in its props, and you can’t read it back. Think of key as a private instruction to React’s reconciler, not as data you’re passing to the element. This is also why the descriptor in the diagram earlier had no key inside props: it lives outside.
Here is the bug that follows from getting the key wrong. The tempting shortcut is to use the array index as the key, since .map hands it to you for free:
{rows.map((row, index) => ( <li key={index}>{row.label}</li>))}This looks completely fine, and it behaves fine, right up until the list is filtered, sorted, reordered, or has an item inserted or removed anywhere but the end. Picture a list where you delete the first row. Every item shifts up a position: what was at index 1 is now at index 0. But React sees key 0 again and concludes it’s the same item as before, so it keeps that DOM node along with any state attached to it and feeds it the new data. The result is that a focused input, a half-typed value, or a checked checkbox stays pinned to position 0 while the data underneath it moved, so state lands on the wrong row. This is a real production bug, not a style nitpick, and it’s why the key has to be tied to the data’s identity, never its position.
See it side by side:
{rows.map((row, index) => ( <li key={index}> <input defaultValue={row.label} /> </li>))}The bug. Delete the top row and every item shifts up an index. React sees the same keys (0, 1, …) and reuses the same DOM nodes, so whatever the user typed into the input stays pinned to its position, not to its row. The data moved; the input state didn’t.
{rows.map((row) => ( <li key={row.id}> <input defaultValue={row.label} /> </li>))}The fix. The key now travels with the row. When the top row is deleted, React matches each surviving row to its previous DOM node by id, and the input state follows the correct row.
What if your data has no natural ID, such as a list you built in memory or rows from a source that didn’t include one? Generate a stable ID and attach it once, when the item is created, then reuse it. At creation time, crypto.randomUUID() (which you met when we covered Web Crypto) gives you one. What you must never do is generate the ID during render: key={Math.random()} produces a brand-new key on every render, so React thinks every item is new every time and constantly throws away all the DOM nodes. The index is out for the same reason. The key has to be as stable as the item it identifies.
Now it’s your turn. The starter below maps over a list with the index as the key. Fix it.
This task list uses the array index as its key. Each row has an uncontrolled input pre-filled with its label. Give each item a stable key tied to the data instead — then the test that deletes the top row will pass, because the input state follows the right row instead of sticking to its position.
Show the answer
Swap the index for the task’s own id:
{tasks.map((task) => ( <li key={task.id}> <input defaultValue={task.label} className="border px-2 py-1" /> </li>))}The index parameter is no longer needed once the key is task.id. The key now travels with the row, so when the top task is removed React matches each surviving row to its previous DOM node by identity — and the input state follows the correct row instead of staying pinned to position 0.
Conditional rendering and the 0 trap
Section titled “Conditional rendering and the 0 trap”Often you don’t want to render a fixed list; you want to render something only sometimes. JSX gives you two idioms for that, and they fall straight out of the render table.
For a one-branch decision (render this, or render nothing) use &&:
{isAdmin && <AdminPanel />}When isAdmin is truthy, && evaluates to its right side and the <AdminPanel /> descriptor renders. When isAdmin is falsy, && short-circuits to the left side, false, and false renders nothing. That’s the whole trick, and it only works because false renders nothing. The conditional-rendering idiom is built directly on that row of the table.
For a two-branch decision (render this or that) use a ternary:
{user ? <Dashboard /> : <SignInPrompt />}If user exists you get the dashboard; otherwise the sign-in prompt. It reads as the sentence it is.
This brings us to the famous trap, which is worth walking through slowly. Say you want to render a list only when it has items. The intuitive approach:
{items.length && <List items={items} />}When items has entries, items.length is a positive number, which is truthy, and the list renders. So far so good. But when items is empty, items.length is 0. And 0 is falsy, so && short-circuits and evaluates to 0: not false, but the actual number 0. React then renders that 0 as a text node, and a stray, inexplicable 0 appears on your page where you expected nothing at all.
This is exactly where the distinction from earlier pays off. false renders nothing; 0 renders, because it’s a number. items.length && ... quietly swaps one for the other the instant the list is empty. Before you read the fix, predict the output:
The cart is empty. What text appears on the page when this component renders? Predict what this program prints, then press Check.
function CartBadge({ items }) { return <div>{items.length && <p>You have items</p>}</div>;}
// Rendered with an empty cart:<CartBadge items={[]} />;items.length is 0, which is falsy, so && short-circuits and evaluates to 0 itself — not false. Numbers render as text (and 0 is a number), while false, null, and undefined render nothing. So the literal 0 paints. Coerce the left side to a real boolean — items.length > 0 && … — and the falsy branch becomes false, which renders nothing.The fix is to make sure the left side of && is a real boolean, never a number that happens to be falsy. Compare it explicitly:
{items.length > 0 && <List items={items} />}Now the left side is true or false, never 0, and the empty case renders nothing as intended. Boolean(items.length) works too, as does switching to a ternary with an explicit empty branch. This is the project’s rule: reach for condition && <Node /> only when condition is genuinely a boolean. The moment the left side is a number, coerce it first.
Fragments group siblings without a wrapper
Section titled “Fragments group siblings without a wrapper”A JSX expression has to evaluate to a single descriptor, so it has one root. Try to return two siblings side by side and you get a syntax error, because two adjacent expressions aren’t one value:
return ( <h1>Title</h1> <p>Body</p>);The newcomer’s instinct is to wrap them in a <div> to make a single root. That compiles, but it’s the wrong fix. The <div> is now a real node in the DOM that exists for no reason but to satisfy JSX, and an extra node isn’t free. It can break a CSS layout, because a flex or grid parent expects its children to be direct descendants, and an unexpected <div> between them severs that relationship. It can also break structure that the browser and assistive tech care about: a <div> wedged between a <ul> and its <li>, or a <tr> and its <td>, is invalid and gets announced wrong.
The right fix is a fragment, <>...</>, which groups siblings into one root without emitting any DOM node at all:
return ( <div> <h1>Title</h1> <p>Body</p> </div>);Compiles, but the <div> is now a real DOM node that exists only to satisfy the one-root rule. It can sever a flex or grid parent from its children, and it inserts an invalid node into structures like <ul>/<li>.
return ( <> <h1>Title</h1> <p>Body</p> </>);<>…</> groups the siblings into a single root and emits no DOM node, so the <h1> and <p> render as direct siblings, exactly as written. This is the default to reach for.
One edge case to recognize: the shorthand <>...</> can’t carry a key. So when the thing you return from a .map is itself a fragment of siblings, you write the long form <React.Fragment key={...}> to give it one. You won’t need it often, but you’ll recognize it when you see it.
Void elements must self-close
Section titled “Void elements must self-close”Some HTML elements have no children and no closing tag. An <img> holds nothing between an opening and closing tag, because there’s nothing to hold. HTML calls these void elements. JSX has one firm rule about them: they must self-close, with the trailing slash.
<img src="/logo.svg" alt="Logo" /><input type="email" /><br /><hr />Drop the slash and you get a JSX parse error: <img src="/logo.svg"> sends the parser looking for a closing </img> that can never come, and the whole expression fails to compile. It’s a frequent first-week stumble, and the error message doesn’t always make the cause obvious, so learn to recognize the shape.
The four you’ll meet daily are <img>, <input>, <br>, and <hr>. The full void set you might encounter across this course is <img>, <input>, <br>, <hr>, <meta>, <link>, <source>, <col>, <area>, <base>, <embed>, <track>, and <wbr>, listed for recognition, not memorization. (Non-void elements may self-close too when they have no children, so <div /> is legal, but the common case there is a childless component, <SignInForm />.)
The edges: comments, types, children, escaping, and style
Section titled “The edges: comments, types, children, escaping, and style”A handful of smaller facts round out the JSX surface. None is big enough for its own section, but each is something an experienced engineer knows and a newcomer trips on, so here they are together.
Comments live in braces too
Section titled “Comments live in braces too”Inside a JSX tree, a comment is just another expression slot: {/* ... */}. Everywhere else in the file, outside the markup, you use the ordinary // and /* */ you already know.
return ( <ul> {/* one row per active subscription */} {subscriptions.map((sub) => ( <li key={sub.id}>{sub.plan}</li> ))} </ul>);The trap: a bare // inside the JSX tree doesn’t comment anything out. It renders as literal text on the page, because to the parser it’s just a string in a child slot. Inside the markup, always use the brace form.
TypeScript types the JSX surface
Section titled “TypeScript types the JSX surface”Earlier I promised that TypeScript catches the rename typos. Here’s the mechanism. Built-in HTML elements are typed through a registry called JSX.IntrinsicElements , so <button> knows exactly which props it takes and what type each one is. Your own components are typed by their declared prop type. The payoff is concrete: write classname instead of className on a <div>, and TypeScript flags it as an error before the code runs. The rename bugs from earlier are caught at author time. That type information is also what powers the prop autocomplete you get in your editor.
You can feel the types directly. Hover a prop and TypeScript tells you what it expects:
<button className="btn" onClick={handleSave}> Save</button>That second type is worth a glance: onClick doesn’t accept “anything.” It accepts a specific kind of function, and TypeScript would reject a string there. The “value is a function” rule from earlier isn’t just convention; the type enforces it.
children arrives as a prop
Section titled “children arrives as a prop”When you nest content between a component’s opening and closing tags, that content arrives inside the component as a prop named children. <Card>hello</Card> calls Card with children: 'hello'. You’ll see children referenced in this chapter’s later examples, the root layout especially, so recognize it for now. The full pattern of designing components around children comes in the components chapter.
JSX escapes by default; dangerouslySetInnerHTML is the opt-out
Section titled “JSX escapes by default; dangerouslySetInnerHTML is the opt-out”This one is a quiet security feature you get for free. Text children are HTML-escaped automatically. If comment holds the string <script>steal()</script>, then <p>{comment}</p> renders it as visible text, the literal characters <script>..., not as a script tag the browser executes. React treats interpolated content as data, never as markup, which shuts the door on a whole class of cross-site scripting attacks before you’ve done anything.
The deliberate escape hatch, for the rare time you do have a trusted HTML string to inject, is dangerouslySetInnerHTML:
<article dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />The name is intentionally alarming, and the {{ __html: ... }} shape is intentionally awkward. React makes the unsafe path look unsafe so you can’t reach for it by accident. Its only legitimate uses are HTML you’ve already sanitized: Markdown rendered through a library like react-markdown, or CMS rich text run through a sanitizer like dompurify. Hand it raw user input and you’ve reopened the exact XSS hole the default escaping closed. The full security treatment comes much later, in the input-security chapter. For now, recognize the name and respect the warning baked into it.
The style prop is an object, not a string
Section titled “The style prop is an object, not a string”In HTML, inline styles are a string: style="margin-top: 8px". In JSX, style takes an object with camelCase property names:
<div style={{ marginTop: 8 }} />Note the double braces: the outer {} is the expression slot, and the inner {} is the object literal. Property names are camelCased (marginTop, not margin-top), and numeric values get px added where it makes sense. That said, you’ll reach for style rarely in this course. Tailwind utility classes, coming in the next chapter, are the default way you’ll style everything. Inline style is reserved for genuinely dynamic values that can’t be expressed as a utility class, like a transform computed from JavaScript at runtime. Recognize it, but don’t reach for it.
Recap: the four bug classes
Section titled “Recap: the four bug classes”Everything in this lesson comes down to one frame: you write JSX, the browser receives HTML, and the differences between them are where bugs hide. Four of those differences are worth committing to memory until you catch them automatically. When you read a diff or review your own code, these are what your eye should snag on.
className, notclass.htmlFor, notfor. The two most common JSX typos, both renamed because the HTML attribute is a reserved word. TypeScript catches them at author time, but learn to spot them anyway.- Keys tied to data identity, never the array index.
key={row.id}, notkey={index}. The index looks fine until the list reorders, and then state lands on the wrong row. - The
&&0-trap.{items.length && ...}paints a stray0when the array is empty, because0is falsy and renders. Coerce numeric left-hand sides to a boolean:items.length > 0 && .... - Pass the handler, don’t call it.
onClick={handleSave}hands React the function;onClick={handleSave()}calls it on every render and is almost always a bug. Wrap it in an arrow when you need arguments:onClick={() => removeRow(row.id)}.
You’re reviewing a teammate’s pull request. Each line below is lifted from a different file. Which lines ship a bug? Select all that apply.
<label for="email">Email</label><button onClick={openModal}>Open settings</button><span>{cart.length && <CartCount />}</span><input type="email" autoComplete="email" />The two buggy lines are <label for="email"> and <span>{cart.length && <CartCount />}</span>.
for="email"—foris a reserved word in JavaScript, so the DOM exposes it ashtmlFor. Written asfor, React ignores it and the label no longer points at the input. The fix ishtmlFor="email".cart.length && …— the0-trap. When the cart is empty,cart.lengthis0, which is falsy and renders, so a stray0paints inside the<span>. Coerce the left side:cart.length > 0 && ….onClick={openModal}is correct — it hands React the function by reference to call on click. Adding parentheses (openModal()) would be the bug, calling it on every render.<input type="email" autoComplete="email" />is correct — a void element self-closed with the trailing slash, andautoCompleteis the right camelCased prop name.
External resources
Section titled “External resources”These pick up where the lesson leaves off. The React docs go deeper on the patterns above, and the Babel REPL lets you watch the JSX transform happen live.