|$ curl https://forge-ai.dev/api/markdown?path=docs/react/jsx
$cat docs/react-—-jsx-deep-dive.md
updated Last week·16 min read·published
React — JSX Deep Dive
What is JSX?
JSX is a syntax extension for JavaScript that lets you write HTML-like code inside your JavaScript. It is not valid JavaScript on its own — compilers like Babel or SWC transform JSX into regular function calls before your code runs. React uses this transformation to create virtual DOM elements.
JSX produces React elements — plain objects describing what should appear on screen. Understanding how JSX compiles helps you write more predictable code and debug rendering issues.
JSX Compilation
jsx_compilation.tsx
TypeScript
| 1 | // JSX you write: |
| 2 | const element = <h1 className="title">Hello, world!</h1>; |
| 3 | |
| 4 | // What Babel/SWC compiles it to: |
| 5 | const element = React.createElement("h1", { className: "title" }, "Hello, world!"); |
| 6 | |
| 7 | // createElement returns a plain object: |
| 8 | // { |
| 9 | // type: "h1", |
| 10 | // props: { |
| 11 | // className: "title", |
| 12 | // children: "Hello, world!" |
| 13 | // }, |
| 14 | // key: null, |
| 15 | // ref: null |
| 16 | // } |
| 17 | |
| 18 | // Multiple children: |
| 19 | const list = ( |
| 20 | <ul> |
| 21 | <li>First</li> |
| 22 | <li>Second</li> |
| 23 | </ul> |
| 24 | ); |
| 25 | |
| 26 | // Compiles to: |
| 27 | const list = React.createElement( |
| 28 | "ul", |
| 29 | null, |
| 30 | React.createElement("li", null, "First"), |
| 31 | React.createElement("li", null, "Second") |
| 32 | ); |
| 33 | |
| 34 | // Custom components — type is the component function/class |
| 35 | const app = <App title="Dashboard" />; |
| 36 | |
| 37 | // Compiles to: |
| 38 | const app = React.createElement(App, { title: "Dashboard" }); |
| 39 | |
| 40 | // JSX must have a single root element |
| 41 | // ❌ Invalid: |
| 42 | // return ( |
| 43 | // <h1>Title</h1> |
| 44 | // <p>Paragraph</p> |
| 45 | // ); |
| 46 | |
| 47 | // ✅ Valid — wrap in a div or fragment: |
| 48 | return ( |
| 49 | <> |
| 50 | <h1>Title</h1> |
| 51 | <p>Paragraph</p> |
| 52 | </> |
| 53 | ); |
ℹ
info
JSX compiles to React.createElement() calls. With the automatic JSX runtime in modern React (17+), you don't need to import Reactin every file — it's injected automatically.
Expressions in JSX
expressions.tsx
TSX
| 1 | // Curly braces {} embed any JavaScript expression in JSX |
| 2 | |
| 3 | // Variables |
| 4 | const name = "Alice"; |
| 5 | const element = <h1>Hello, {name}</h1>; |
| 6 | |
| 7 | // Function calls |
| 8 | function formatDate(date: Date): string { |
| 9 | return date.toLocaleDateString(); |
| 10 | } |
| 11 | const dateElement = <p>Today is {formatDate(new Date())}</p>; |
| 12 | |
| 13 | // Arithmetic |
| 14 | const price = 19.99; |
| 15 | const tax = 0.08; |
| 16 | const total = <span>Total: ${(price * (1 + tax)).toFixed(2)}</span>; |
| 17 | |
| 18 | // Ternary expressions |
| 19 | const isLoggedIn = true; |
| 20 | const greeting = <h1>{isLoggedIn ? "Welcome back!" : "Please sign in"}</h1>; |
| 21 | |
| 22 | // Template literals |
| 23 | const user = "Alice"; |
| 24 | const role = "Admin"; |
| 25 | const header = <h2>{`${role}: ${user}`}</h2>; |
| 26 | |
| 27 | // Object spread in props |
| 28 | const baseProps = { className: "card", id: "main-card" }; |
| 29 | const card = <div {...baseProps} data-testid="card-1" />; |
| 30 | |
| 31 | // Array methods — map returns arrays of elements |
| 32 | const items = ["React", "TypeScript", "Next.js"]; |
| 33 | const list = ( |
| 34 | <ul> |
| 35 | {items.map((item) => ( |
| 36 | <li key={item}>{item}</li> |
| 37 | ))} |
| 38 | </ul> |
| 39 | ); |
| 40 | |
| 41 | // Children as expressions |
| 42 | function Card({ children }: { children: React.ReactNode }) { |
| 43 | return <div className="card">{children}</div>; |
| 44 | } |
| 45 | |
| 46 | const cardWithContent = ( |
| 47 | <Card> |
| 48 | <h2>Title</h2> |
| 49 | <p>{someCondition ? "Yes" : "No"}</p> |
| 50 | </Card> |
| 51 | ); |
| 52 | |
| 53 | // Inline styles — object, not string |
| 54 | const style = { color: "red", fontSize: "16px" }; |
| 55 | const colored = <p style={style}>Red text</p>; |
Fragments
fragments.tsx
TSX
| 1 | // Fragments let you group children without adding extra DOM nodes |
| 2 | |
| 3 | // Short syntax |
| 4 | function List() { |
| 5 | return ( |
| 6 | <> |
| 7 | <dt>Term 1</dt> |
| 8 | <dd>Definition 1</dd> |
| 9 | <dt>Term 2</dt> |
| 10 | <dd>Definition 2</dd> |
| 11 | </> |
| 12 | ); |
| 13 | } |
| 14 | |
| 15 | // Explicit Fragment syntax — needed when you need a key prop |
| 16 | import { Fragment } from "react"; |
| 17 | |
| 18 | function Glossary({ items }: { items: { term: string; def: string }[] }) { |
| 19 | return ( |
| 20 | <dl> |
| 21 | {items.map((item) => ( |
| 22 | <Fragment key={item.term}> |
| 23 | <dt>{item.term}</dt> |
| 24 | <dd>{item.def}</dd> |
| 25 | </Fragment> |
| 26 | ))} |
| 27 | </dl> |
| 28 | ); |
| 29 | } |
| 30 | |
| 31 | // Fragment does NOT accept props (no key, className, style) |
| 32 | // ❌ Invalid: |
| 33 | // <><h1 className="title">Title</h1></> |
| 34 | |
| 35 | // ✅ Use a div or a wrapper component instead: |
| 36 | // <div className="title"><h1>Title</h1></div> |
| 37 | |
| 38 | // Custom Fragment wrapper for layout |
| 39 | function Row({ children }: { children: React.ReactNode }) { |
| 40 | return <div style={{ display: "flex", gap: "1rem" }}>{children}</div>; |
| 41 | } |
| 42 | |
| 43 | function Layout() { |
| 44 | return ( |
| 45 | <Row> |
| 46 | <aside>Sidebar</aside> |
| 47 | <main>Content</main> |
| 48 | </Row> |
| 49 | ); |
| 50 | } |
| 51 | |
| 52 | // Fragment in table rows — common use case |
| 53 | function Table({ rows }: { rows: { name: string; age: number }[] }) { |
| 54 | return ( |
| 55 | <table> |
| 56 | <tbody> |
| 57 | {rows.map((row) => ( |
| 58 | <Fragment key={row.name}> |
| 59 | <tr> |
| 60 | <td>{row.name}</td> |
| 61 | <td>{row.age}</td> |
| 62 | </tr> |
| 63 | </Fragment> |
| 64 | ))} |
| 65 | </tbody> |
| 66 | </table> |
| 67 | ); |
| 68 | } |
Conditional Rendering
conditional.tsx
TSX
| 1 | // Multiple patterns for conditional rendering |
| 2 | |
| 3 | // 1. if/else — simplest, good for complex conditions |
| 4 | function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) { |
| 5 | if (isLoggedIn) { |
| 6 | return <h1>Welcome back!</h1>; |
| 7 | } |
| 8 | return <h1>Please sign in.</h1>; |
| 9 | } |
| 10 | |
| 11 | // 2. Ternary — inline conditional |
| 12 | const message = <p>{isLoggedIn ? "Welcome" : "Sign in"}</p>; |
| 13 | |
| 14 | // 3. Logical AND — render or nothing |
| 15 | const notification = hasNotification && <Badge count={count} />; |
| 16 | |
| 17 | // 4. Variable assignment — for complex JSX |
| 18 | function Status({ status }: { status: "loading" | "error" | "success" }) { |
| 19 | let content: React.ReactNode; |
| 20 | |
| 21 | if (status === "loading") { |
| 22 | content = <Spinner />; |
| 23 | } else if (status === "error") { |
| 24 | content = <ErrorMessage />; |
| 25 | } else { |
| 26 | content = <SuccessMessage />; |
| 27 | } |
| 28 | |
| 29 | return <div className="status">{content}</div>; |
| 30 | } |
| 31 | |
| 32 | // 5. Early return — guard clause |
| 33 | function UserCard({ user }: { user: User | null }) { |
| 34 | if (!user) return null; |
| 35 | |
| 36 | return ( |
| 37 | <div> |
| 38 | <h2>{user.name}</h2> |
| 39 | <p>{user.email}</p> |
| 40 | </div> |
| 41 | ); |
| 42 | } |
| 43 | |
| 44 | // 6. Render props pattern — composition over conditionals |
| 45 | function Maybe({ |
| 46 | condition, |
| 47 | children, |
| 48 | }: { |
| 49 | condition: boolean; |
| 50 | children: React.ReactNode; |
| 51 | }) { |
| 52 | return condition ? <>{children}</> : null; |
| 53 | } |
| 54 | |
| 55 | // Usage |
| 56 | <Maybe condition={isLoggedIn}> |
| 57 | <Dashboard /> |
| 58 | </Maybe> |
| 59 | |
| 60 | // 7. Switch-like pattern with object mapping |
| 61 | type Status = "idle" | "loading" | "success" | "error"; |
| 62 | |
| 63 | const statusComponents: Record<Status, React.FC> = { |
| 64 | idle: IdleScreen, |
| 65 | loading: LoadingSpinner, |
| 66 | success: SuccessScreen, |
| 67 | error: ErrorScreen, |
| 68 | }; |
| 69 | |
| 70 | function StatusView({ status }: { status: Status }) { |
| 71 | const Component = statusComponents[status]; |
| 72 | return <Component />; |
| 73 | } |
Lists and Keys
lists_keys.tsx
TSX
| 1 | // Keys help React identify which items changed, added, or removed |
| 2 | |
| 3 | // ✅ Good — use stable, unique IDs |
| 4 | interface Todo { |
| 5 | id: string; |
| 6 | text: string; |
| 7 | completed: boolean; |
| 8 | } |
| 9 | |
| 10 | function TodoList({ todos }: { todos: Todo[] }) { |
| 11 | return ( |
| 12 | <ul> |
| 13 | {todos.map((todo) => ( |
| 14 | <li key={todo.id}> |
| 15 | <input type="checkbox" checked={todo.completed} /> |
| 16 | {todo.text} |
| 17 | </li> |
| 18 | ))} |
| 19 | </ul> |
| 20 | ); |
| 21 | } |
| 22 | |
| 23 | // ❌ Bad — using index as key (causes issues with reorder/delete) |
| 24 | // {todos.map((todo, index) => ( |
| 25 | // <li key={index}>{todo.text}</li> |
| 26 | // ))} |
| 27 | |
| 28 | // Why index keys are bad: |
| 29 | // If you delete item 1 from [A, B, C] → [A, C] |
| 30 | // React sees key 0 (A→A, skip), key 1 (B→C, update), key 2 (removed) |
| 31 | // But C didn't change — B was deleted. React updates the wrong element. |
| 32 | |
| 33 | // Nested lists — flatten or use compound keys |
| 34 | interface Category { |
| 35 | id: string; |
| 36 | name: string; |
| 37 | items: { id: string; name: string }[]; |
| 38 | } |
| 39 | |
| 40 | function CategoryList({ categories }: { categories: Category[] }) { |
| 41 | return ( |
| 42 | <div> |
| 43 | {categories.map((category) => ( |
| 44 | <div key={category.id}> |
| 45 | <h3>{category.name}</h3> |
| 46 | <ul> |
| 47 | {category.items.map((item) => ( |
| 48 | <li key={`${category.id}-${item.id}`}>{item.name}</li> |
| 49 | ))} |
| 50 | </ul> |
| 51 | </div> |
| 52 | ))} |
| 53 | </div> |
| 54 | ); |
| 55 | } |
| 56 | |
| 57 | // Filtering and reordering with keys |
| 58 | function FilteredList({ items }: { items: string[] }) { |
| 59 | const [filter, setFilter] = React.useState(""); |
| 60 | |
| 61 | const filtered = items.filter((item) => |
| 62 | item.toLowerCase().includes(filter.toLowerCase()) |
| 63 | ); |
| 64 | |
| 65 | return ( |
| 66 | <div> |
| 67 | <input value={filter} onChange={(e) => setFilter(e.target.value)} /> |
| 68 | <ul> |
| 69 | {filtered.map((item) => ( |
| 70 | <li key={item}>{item}</li> |
| 71 | ))} |
| 72 | </ul> |
| 73 | </div> |
| 74 | ); |
| 75 | } |
⚠
warning
Never use array indices as keys when the list can be reordered, filtered, or have items inserted/deleted. Use stable unique identifiers from your data instead.
Spread Attributes
spread.tsx
TSX
| 1 | // Spread syntax passes an object's properties as props |
| 2 | |
| 3 | // Base props object |
| 4 | const buttonProps = { |
| 5 | type: "submit" as const, |
| 6 | className: "btn btn-primary", |
| 7 | disabled: false, |
| 8 | }; |
| 9 | |
| 10 | // Spread into JSX |
| 11 | <button {...buttonProps}>Submit</button> |
| 12 | // Compiles to: React.createElement("button", { ...buttonProps }, "Submit") |
| 13 | |
| 14 | // Override spread props — later props win |
| 15 | <button {...buttonProps} disabled={true} className="btn-danger"> |
| 16 | Delete |
| 17 | </button> |
| 18 | // type="submit", className="btn-danger" (overridden), disabled=true (overridden) |
| 19 | |
| 20 | // Forward all props to a child component |
| 21 | function TextInput(props: React.InputHTMLAttributes<HTMLInputElement>) { |
| 22 | return <input {...props} />; |
| 23 | } |
| 24 | |
| 25 | // Usage |
| 26 | <TextInput |
| 27 | type="email" |
| 28 | placeholder="Enter email" |
| 29 | className="input-field" |
| 30 | onChange={(e) => console.log(e.target.value)} |
| 31 | /> |
| 32 | |
| 33 | // Spread with conditional props |
| 34 | interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { |
| 35 | variant?: "primary" | "secondary" | "danger"; |
| 36 | } |
| 37 | |
| 38 | function Button({ variant = "primary", className, ...rest }: ButtonProps) { |
| 39 | return ( |
| 40 | <button |
| 41 | className={`btn btn-${variant} ${className ?? ""}`.trim()} |
| 42 | {...rest} |
| 43 | /> |
| 44 | ); |
| 45 | } |
| 46 | |
| 47 | // Props forwarding pattern |
| 48 | function Dialog({ |
| 49 | title, |
| 50 | content, |
| 51 | ...dialogProps |
| 52 | }: { |
| 53 | title: string; |
| 54 | content: React.ReactNode; |
| 55 | } & React.DialogHTMLAttributes<HTMLDialogElement>) { |
| 56 | return ( |
| 57 | <dialog {...dialogProps}> |
| 58 | <h2>{title}</h2> |
| 59 | <div>{content}</div> |
| 60 | </dialog> |
| 61 | ); |
| 62 | } |
| 63 | |
| 64 | // Spread with refs — use forwardRef |
| 65 | const FancyInput = React.forwardRef<HTMLInputElement, InputProps>( |
| 66 | (props, ref) => <input ref={ref} {...props} className="fancy" /> |
| 67 | ); |
JSX Prevents Injection
prevents_injection.tsx
TSX
| 1 | // JSX escapes values by default — prevents XSS attacks |
| 2 | |
| 3 | // User input is rendered as text, not HTML |
| 4 | const userInput = '<script>alert("xss")</script>'; |
| 5 | const element = <p>{userInput}</p>; |
| 6 | // Renders: <p><script>alert("xss")</script></p> |
| 7 | // NOT as executable script |
| 8 | |
| 9 | // dangerouslySetInnerHTML — opt-in to raw HTML (use with caution) |
| 10 | const rawHtml = '<strong>Bold text</strong>'; |
| 11 | |
| 12 | // ✅ Safe — JSX escapes automatically |
| 13 | const safe = <div>{rawHtml}</div>; |
| 14 | // Renders: <div><strong>Bold text</strong></div> |
| 15 | |
| 16 | // ⚠️ Dangerous — bypasses escaping |
| 17 | const dangerous = ( |
| 18 | <div dangerouslySetInnerHTML={{ __html: rawHtml }} /> |
| 19 | ); |
| 20 | // Renders: <div><strong>Bold text</strong></div> |
| 21 | |
| 22 | // Only use dangerouslySetInnerHTML with trusted content: |
| 23 | function MarkdownRenderer({ content }: { content: string }) { |
| 24 | // Sanitize content first with DOMPurify or similar |
| 25 | const sanitized = DOMPurify.sanitize(content); |
| 26 | return ( |
| 27 | <article |
| 28 | className="prose" |
| 29 | dangerouslySetInnerHTML={{ __html: sanitized }} |
| 30 | /> |
| 31 | ); |
| 32 | } |
| 33 | |
| 34 | // Attribute values are also escaped |
| 35 | const url = 'javascript:alert("xss")'; |
| 36 | // ❌ React does NOT prevent javascript: URLs in href |
| 37 | // <a href={url}>Click</a> — this IS dangerous |
| 38 | |
| 39 | // ✅ Always validate URLs before rendering |
| 40 | function SafeLink({ href, children }: { href: string; children: React.ReactNode }) { |
| 41 | const isSafe = href.startsWith("http://") || href.startsWith("https://") || href.startsWith("/"); |
| 42 | if (!isSafe) { |
| 43 | console.warn(`Blocked unsafe URL: ${href}`); |
| 44 | return <span>{children}</span>; |
| 45 | } |
| 46 | return <a href={href}>{children}</a>; |
| 47 | } |
ℹ
info
JSX automatically escapes all interpolated values, preventing XSS attacks. Never use dangerouslySetInnerHTML with user-provided content without sanitization.
$Blueprint — Engineering Documentation·Section ID: REACT-JSX·Revision: 1.0