02 — JSX Deep Dive
JSX → React.createElement
jsx_to_createelement.js
// JSX is syntactic sugar for React.createElement. The compiler (Babel/SWC)
// transforms it at build time — JSX never reaches the browser.
// WHAT YOU WRITE:
const greeting = <h1 className="title" id="main">Hello, {name}!</h1>
// WHAT THE COMPILER PRODUCES:
const greeting = React.createElement(
'h1', // type: string (DOM) | function (component) | symbol (fragment)
{ className: 'title', id: 'main' }, // props object
'Hello, ', // children...
name,
'!'
)
// WHICH BECOMES THIS OBJECT:
// {
// type: 'h1',
// props: { className: 'title', id: 'main', children: ['Hello, ', name, '!'] },
// key: null,
// ref: null,
// $$typeof: Symbol.for('react.element')
// }
// Knowing this explains WHY:
// - You can store JSX in variables: const el = <div/> (it's just an object)
// - You can pass JSX as props: <Card header={<h1>Title</h1>} />
// - You can return JSX from functions: getIcon(name) → <svg/>
// - You can't use if/for/switch INSIDE JSX — they're statements, not expressions
Expression Embedding Rules
expression_embedding.js
// {} in JSX evaluates a SINGLE EXPRESSION — anything that returns a value.
// You CANNOT put STATEMENTS (if, for, switch, const) inside {}.
// VALID — expressions:
<div>{2 + 2}</div> // arithmetic
<div>{condition ? 'yes' : 'no'}</div> // ternary
<div>{user?.name || 'Anonymous'}</div> // optional chaining + fallback
<div>{items.map(i => <li key={i.id}>{i.name}</li>)}</div> // .map() returns array
<div>{<Child />}</div> // JSX is an expression (it's just an object)
// INVALID — statements (these throw syntax errors):
// <div>{ if (x) { return 'yes' } }</div> // if is a statement
// <div>{ for (let i of items) { ... } }</div> // for is a statement
// <div>{ const val = compute(); val }</div> // const is a statement
// WORKAROUND for conditionals inside JSX: ternary or &&
<div>{show && <Modal/>}</div>
// WORKAROUND for loops: .map() (it returns an array of elements)
<div>{items.map(item => <Row key={item.id} {...item} />)}</div>
// WORKAROUND for complex logic: IIFE or extract to a variable/function
const content = (() => {
if (loading) return <Spinner/>
if (error) return <ErrorMessage error={error}/>
return <DataView data={data}/>
})()
return <div>{content}</div>
Conditional Rendering: Patterns and Gotchas
conditional_gotchas.js
// ANTI-PATTERN: && with numeric values renders 0
function Cart({ count }) {
return <div>{count && <Badge count={count} />}</div>
// When count === 0: renders "0" on screen! 0 is falsy but React renders it.
// && returns the LEFT operand if it's falsy — so 0 && <Badge/> → 0
// React renders numbers (including 0) as text. Only null, undefined, false,
// and true are ignored by React's renderer.
}
// FIX: use a boolean check or ternary
function Cart({ count }) {
return <div>{count > 0 && <Badge count={count} />}</div> // count > 0 → true/false
// OR: {count > 0 ? <Badge count={count} /> : null}
}
// GOTCHA: empty string "" is rendered by React (it's a valid child)
// null, undefined, false, true → NOT rendered (ignored)
// 0, "" → rendered as text
conditional_patterns.js
// PRODUCTION PATTERN: extract complex conditionals for readability
function UserMenu({ user, isLoading, isError }) {
// Each branch is a clean expression — no nested ternaries that are hard to read
if (isLoading) return <Spinner />
if (isError) return <ErrorMessage />
if (!user) return <LoginButton />
return <ProfileDropdown user={user} />
// Early returns OUTSIDE JSX are cleaner than nested ternaries INSIDE JSX.
// Use ternaries for simple 2-way choices within JSX, early returns for
// multi-branch or guard-clause logic.
}
Fragments: Grouping Without Extra DOM
fragments.js
import { Fragment } from 'react'
// PROBLEM: a component must return a single root element, but you don't want
// to add an extra <div> that breaks CSS (flexbox/grid) or semantics.
// SHORT SYNTAX (no key support):
function Columns() {
return (
<>
<td>Hello</td>
<td>World</td>
</>
)
}
// Compiles to React.createElement(React.Fragment, null, ...)
// EXPLICIT SYNTAX (supports key — needed in lists):
function DefinitionList({ terms }) {
return terms.map(([term, def]) => (
<Fragment key={term}>
<dt>{term}</dt>
<dd>{def}</dd>
</Fragment>
))
}
// The short <> syntax CANNOT take a key prop — use <Fragment key={...}> in lists.
// React won't warn about missing keys if you use <> — but it won't be able to
// reconcile items correctly either. Always use explicit Fragment with key in lists.
Spread Attributes: Powerful, Dangerous
spread_pitfalls.js
// CONVENIENT: forward all props to a child
function Button({ variant, ...rest }) {
return <button className={`btn btn-${variant}`} {...rest} />
// rest = all props except variant — className, onClick, disabled, type, etc.
}
// ANTI-PATTERN: spreading unknown props breaks explicit contracts
function UserCard({ name, email, ...unknownProps }) {
// unknownProps could contain ANYTHING — including props UserCard shouldn't receive
return <div {...unknownProps}> // ← div now has arbitrary attributes
<h3>{name}</h3>
<p>{email}</p>
</div>
}
// If someone passes <UserCard onClick={...} />, the div gets an onClick —
// probably not intended. Explicit is safer:
function UserCard({ name, email, onClick }) {
return <div onClick={onClick}>...</div> // only what you intend
}
// GOTCHA: spread overrides earlier props — order matters
<input type="text" {...props} /> // props.type overrides "text" if present
<input {...props} type="text" /> // type="text" always wins (last wins)
Inline Object Literals and Re-render Traps
inline_object_renders.js
// ANTI-PATTERN: inline object/array literals create NEW references every render
function Parent({ data }) {
return <Child
config={{ theme: 'dark', size: 'lg' }} // ← new object every render
items={[1, 2, 3]} // ← new array every render
style={{ color: 'red' }} // ← new object every render
/>
}
// If Child is React.memo'd, the memo BREAKS — new object/array reference every
// render means shallow comparison fails, Child re-renders every time.
// These literals also break useEffect/useCallback dependency arrays (Ch 6, 9).
// FIX: hoist stable references outside the component or use useMemo
const STATIC_CONFIG = { theme: 'dark', size: 'lg' } // module-level constant
const STATIC_ITEMS = [1, 2, 3]
function Parent({ data }) {
return <Child config={STATIC_CONFIG} items={STATIC_ITEMS} />
}
// Now the references are stable — React.memo on Child works correctly.
💡 Tips & Tricks
tips.js
// [Idiom] JSX is just an object — you can store it, pass it, return it from
// functions. Use this to build render helpers without creating components:
// const icon = isLoading ? <Spinner/> : <CheckIcon/>
// [Idiom] Use early returns for guard clauses instead of deep nesting.
// if (!data) return null is cleaner than {data && <div>...</div>} for multi-line.
// [Performance] Hoist stable object/array literals to module scope to avoid
// breaking React.memo on children. Every inline {{...}} creates a new reference.
// [Debug] If a number "0" appears unexpectedly in your UI, check for
// {count && <Component/>} — use {count > 0 && <Component/>} instead.
// [Idiom] <Fragment key={...}> is required when mapping to multiple sibling
// elements per item. The short <> syntax can't take a key.
⚠️ Edge Cases & Gotchas
edge_cases.js
// [Gotcha] && with 0 renders "0" on screen. 0 is falsy but React renders numbers.
// Fix: {count > 0 && <Badge/>} or {count ? <Badge/> : null}
// [Gotcha] Empty string "" is rendered by React. null, undefined, false, true
// are NOT rendered. If you conditionally render an empty string for "hide",
// it still takes up space in some layouts. Use null instead.
// [Gotcha] Spread {...props} overrides earlier explicit props if it comes AFTER
// them. Put spread FIRST if you want explicit props to win, LAST if spread wins.
// [Gotcha] You can't use if/for/while/switch inside {} — they're statements.
// Use ternary, &&, .map(), or extract to a variable/function.
// [Gotcha] JSX comments ({/* */}) are NOT regular HTML comments (<!-- -->).
// HTML comments inside JSX are treated as text and rendered to the DOM.
// Always use {/* comment */} for JSX comments.
🧠 Spot the Bug
A developer renders a list of products but sees a React warning: "Each child in a list should have a unique key prop." The code looks correct:
spot_the_bug.js
function ProductList({ products }) {
return (
<div>
{products.map(product => (
<>
<h3>{product.name}</h3>
<p>{product.price}</p>
</>
))}
</div>
)
}
Answer
The fragment uses the short <> syntax, which cannot accept a key prop. When mapping to fragments, you must use the explicit <Fragment key={...}> form:
import { Fragment } from 'react'
function ProductList({ products }) {
return (
<div>
{products.map(product => (
<Fragment key={product.id}>
<h3>{product.name}</h3>
<p>{product.price}</p>
</Fragment>
))}
</div>
)
}
Without a key on the fragment, React can't track which <h3>+<p> pair corresponds to which product during reconciliation — adding/removing/reordering products will cause incorrect DOM updates.
Key Takeaways
key_takeaways.js
// 1. JSX compiles to React.createElement() → plain element objects. It's an
// expression, so you can store it, pass it, return it from functions.
// 2. {} evaluates ONE expression — no statements (if/for/switch). Use ternary,
// &&, .map(), or extract to a variable/function for complex logic.
// 3. {count && <X/>} renders "0" when count is 0 — use {count > 0 && <X/>}.
// React renders 0 and "" as text; ignores null, undefined, false, true.
// 4. Fragments group siblings without extra DOM. Use <Fragment key={...}> in
// lists — the short <> syntax can't take a key prop.
// 5. Inline object/array literals ({{...}} and {[...]}) create new references
// every render → breaks React.memo, useEffect deps, useCallback deps.
// Hoist stable values to module scope or use useMemo.