Chapter 4: JSX: When HTML and JavaScript Had a Baby Nobody Asked For
JSX is React’s most visible feature and its most controversial. It’s the thing that makes React developers say “it’s just JavaScript!” and everyone else say “that’s definitely not JavaScript.”
JSX is what happens when you can’t decide if you’re writing HTML or JavaScript, so you write both, poorly, at the same time.
XML in JavaScript: What Could Go Wrong?
Let’s start with a simple question: What is JSX?
According to React, JSX is “a syntax extension to JavaScript.” This is like saying a platypus is “a mammal extension to ducks.” Technically true, deeply disturbing.
Here’s JSX in all its glory:
const element = (
<div className="container">
<h1>Hello, {user.name}!</h1>
<p>You have {unreadCount} unread messages.</p>
{unreadCount > 0 && <button onClick={handleClick}>Read Messages</button>}
</div>
);
“It looks just like HTML!” React developers exclaim.
Except:
class
is nowclassName
for
is nowhtmlFor
- You can’t use
style
as a string - Self-closing tags are mandatory for elements without children
- JavaScript expressions go in curly braces
- Conditional rendering uses JavaScript operators
- It’s actually function calls in disguise
So it’s exactly like HTML, except for all the ways it’s different.
The Transpilation Tax
Here’s the dirty secret about JSX: browsers can’t understand it. At all. Your beautiful JSX needs to be transformed into regular JavaScript before it can run.
What you write:
<div className="hello">
<h1>Hello, {name}</h1>
</div>
What actually runs:
React.createElement(
'div',
{ className: 'hello' },
React.createElement(
'h1',
null,
'Hello, ',
name
)
)
Every. Single. Element. Becomes. A. Function. Call.
This means you need Babel or another transpiler. Which means you need a build step. Which means you need a build configuration. Which means you need webpack or Vite or whatever the build tool du jour is.
All so you can write HTML-ish syntax in JavaScript instead of just writing HTML.
Why className and htmlFor Exist
My favorite part of JSX is the inconsistency. React couldn’t use class
because it’s a reserved word in JavaScript. Fair enough. But then why does this work?
<div data-class="something"> // Works fine
<div aria-label="something"> // Also fine
<div className="something"> // Required for CSS classes
The same goes for for
:
<label htmlFor="input-id"> // Can't use 'for'
<label data-for="input-id"> // But this is fine
It’s almost like they could have figured out a way to make class
and for
work, but chose not to. Because suffering builds character.
Conditional Rendering: Ternary Operators Everywhere
Want to conditionally show something in HTML? Simple:
<!-- Server-side templating -->
<?php if ($isLoggedIn): ?>
<p>Welcome back!</p>
<?php endif; ?>
Want to do it in JSX? Hope you like ternary operators:
{isLoggedIn ? <p>Welcome back!</p> : null}
// Or the && operator hack
{isLoggedIn && <p>Welcome back!</p>}
// Or for multiple conditions
{isLoggedIn ? (
<p>Welcome back!</p>
) : isGuest ? (
<p>Welcome, guest!</p>
) : (
<p>Please log in</p>
)}
That last one is what we in the business call “ternary hell.” It’s like regular hell, but with more question marks.
The Style Attribute Disaster
In HTML, styles are strings:
<div style="color: red; font-size: 16px;">Hello</div>
In JSX, styles are objects with camelCased properties:
<div style=>Hello</div>
Notice the double curly braces? That’s not a typo. The outer braces say “here comes JavaScript” and the inner braces are the object literal. It’s braces all the way down.
Also, notice how font-size
became fontSize
? That’s because JavaScript doesn’t like hyphens in property names. So every CSS property you know needs to be translated to camelCase.
background-color
→ backgroundColor
margin-top
→ marginTop
border-bottom-left-radius
→ borderBottomLeftRadius
It’s like React took CSS and said, “This isn’t confusing enough. Let’s make developers translate every property name in their heads.”
Comments: The Final Insult
Want to add a comment in HTML?
<!-- This is a comment -->
Want to add a comment in JSX?
{/* This is a comment */}
Because of course comments need to be wrapped in JSX expression syntax and use JavaScript comment syntax. It’s comments with extra steps!
But wait, it gets worse. This doesn’t work:
<div>
// This comment breaks everything
<p>Hello</p>
</div>
Neither does this:
<div>
<!-- This also breaks -->
<p>Hello</p>
</div>
You must use the special JSX comment syntax, or your app won’t compile. Because nothing says “developer friendly” like making comments complicated.
Arrays and Keys: React’s Obsession
When rendering lists in JSX, every element needs a unique key
prop:
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
Forget the key? React will yell at you in the console. Use the array index as a key? React will passive-aggressively warn you that it’s not recommended.
“But keys help React identify which items have changed!” React developers explain.
You know what else identifies which items have changed? The fact that they changed. If I update item 3, item 3 changed. This isn’t quantum physics.
The Fragment Nonsense
JSX components must return a single element. Want to return two elements? Too bad. Wrap them in a meaningless div:
// This doesn't work
return (
<h1>Title</h1>
<p>Paragraph</p>
);
// This does
return (
<div>
<h1>Title</h1>
<p>Paragraph</p>
</div>
);
“But that adds unnecessary DOM nodes!” you cry. React hears you and offers Fragments:
return (
<React.Fragment>
<h1>Title</h1>
<p>Paragraph</p>
</React.Fragment>
);
// Or the shorthand that looks like broken HTML
return (
<>
<h1>Title</h1>
<p>Paragraph</p>
</>
);
Yes, <>
and </>
are valid JSX. It’s an empty tag that doesn’t exist but also does. Schrödinger’s HTML element.
Whitespace Handling: The Invisible Enemy
JSX handles whitespace differently than HTML:
// This renders as "HelloWorld" (no space)
<div>
Hello
World
</div>
// This renders as "Hello World" (with space)
<div>
Hello{' '}
World
</div>
That’s right, you sometimes need {' '}
to add a space. A space! The thing you get by pressing the largest key on your keyboard now requires special syntax.
Event Handlers: Almost Normal, But Not
HTML event handlers:
<button onclick="handleClick()">Click me</button>
JSX event handlers:
<button onClick={handleClick}>Click me</button>
Notice the differences:
onclick
→onClick
(camelCase)- String → JavaScript expression
- Function call → Function reference
Pass the wrong thing and enjoy your errors:
<button onClick={handleClick()}>Click me</button>
// Executes immediately, not on click
<button onClick="handleClick">Click me</button>
// String is not a function error
The Boolean Attribute Trap
In HTML, boolean attributes work like this:
<input disabled>
<input disabled="disabled">
<input disabled="">
<!-- All three are disabled -->
In JSX:
<input disabled={true} /> // Disabled
<input disabled={false} /> // Not disabled
<input disabled /> // Disabled (shorthand for true)
<input disabled="" /> // Disabled (empty string is truthy for attributes)
<input disabled={0} /> // Not disabled (0 is falsy)
The rules are:
true
means truefalse
means false- No value means true
- Empty string means true (wat?)
- 0 means false
null
orundefined
means the attribute isn’t rendered
Clear as mud, right?
XSS Protection: The One Good Thing
Okay, I’ll give React this one: JSX automatically escapes values to prevent XSS attacks:
const userInput = '<script>alert("XSS")</script>';
return <div>{userInput}</div>;
// Renders as text, not as a script tag
This is genuinely good. Of course, when you actually want to render HTML, you need to use dangerouslySetInnerHTML
:
<div dangerouslySetInnerHTML= />
Yes, that’s the actual prop name. It’s like React is passive-aggressively saying, “Fine, render your HTML, but we’re going to make you type ‘dangerous’ every time so you know we disapprove.”
The Reality Check
Here’s what we gave up for JSX:
<!-- Before: Actual HTML -->
<div class="card">
<h2>Title</h2>
<p>Content</p>
<!-- Easy comment -->
<button onclick="handleClick()">Click</button>
</div>
Here’s what we got:
// After: JSX
<div className="card">
<h2>Title</h2>
<p>Content</p>
{/* Complicated comment */}
<button onClick={handleClick}>Click</button>
</div>
The differences seem small, but they add up. Every HTML attribute you know needs to be relearned. Every pattern you’re familiar with needs to be translated. Every simple thing becomes slightly more complex.
In Defense of JSX (It Hurts to Say This)
JSX does have some benefits:
- Component Composition: Combining components is intuitive
- Type Checking: With TypeScript, you get prop validation
- JavaScript Power: Full programming language in your templates
- Tooling Support: Excellent IDE support and error messages
But we could have had these benefits without creating a whole new syntax that’s neither HTML nor JavaScript.
The Alternatives That Could Have Been
Other frameworks figured this out:
<!-- Vue: Actual HTML with directives -->
<div v-if="isVisible" @click="handleClick">
</div>
<!-- Svelte: HTML with minimal additions -->
{#if isVisible}
<div on:click={handleClick}>
{message}
</div>
{/if}
Both are closer to HTML. Both are easier to learn. Both don’t require translating attribute names in your head.
Accepting Your Fate
JSX is here to stay. It’s become so synonymous with React that most developers can’t imagine React without it. Even though you can use React without JSX:
// React without JSX (nobody does this)
React.createElement('div', { className: 'hello' },
React.createElement('h1', null, 'Hello, World')
)
But nobody does this because it’s verbose and painful. Which is ironic, because that’s exactly what your JSX becomes after transpilation.
The Path Forward
You’re going to write JSX. You’re going to forget that it’s className
not class
. You’re going to mess up the style object syntax. You’re going to wonder why your comment broke everything.
And eventually, Stockholm syndrome will set in. You’ll start to like JSX. You’ll defend it in forums. You’ll say things like “it’s just JavaScript!” and mean it.
That’s when you’ll know React has won. When the abnormal becomes normal. When the complex becomes “simple.” When writing HTML in JavaScript with different attribute names and special syntax for everything seems perfectly reasonable.
Welcome to JSX. It’s like HTML and JavaScript had a baby, and that baby was raised by wolves who really liked curly braces.
In the next chapter, we’ll explore components - React’s answer to the question “What if everything was a function that returned other functions that returned HTML-ish JavaScript?”
But first, take a moment to remember when HTML was HTML and JavaScript was JavaScript, and never the twain shall meet. Those were simpler times.
Now excuse me while I debug why class="container"
isn’t working. Oh right, className
. Gets me every time.