Skip to main content

🧬 JSX Syntax and Props

JSX lets you write markup right inside JavaScript, and props let you feed data into it. Together they are how you describe a React UI. This lesson covers JSX's rules and quirks, then goes deep on passing, defaulting, and forwarding props.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what JSX is, how Babel compiles it, and its core syntax rules
  • Embed JavaScript expressions in JSX and apply conditional rendering (&&, ternary)
  • Render lists with .map() and correct keys, and group elements with Fragments
  • Pass, destructure, and default props, and forward extra props with the spread operator
  • Use the children prop and avoid common JSX pitfalls

Estimated Time: 35–45 minutes  •  Difficulty: Intermediate

Hands-on: Build a reusable, prop-driven product card that renders a list of products.

In This Lesson

What Is JSX?

JSX (JavaScript XML) is a syntax extension that lets you write HTML-like markup directly in your JavaScript. It is not required to use React, but nearly everyone does — it makes component structure far easier to read than the alternative.

Browsers can't run JSX directly. A compiler — usually Babel, built into tools like Vite and Next.js — transpiles it into plain React.createElement calls (or the modern jsx() runtime) before it reaches the browser:

flowchart LR A["JSX you write"] -->|Babel compiles| B["createElement calls"] B -->|React renders| C[Virtual DOM] C -->|Reconciliation| D[Real DOM]

Here is the same component written both ways. The JSX version on top compiles to the JavaScript below it:

// What you write (JSX)
function Welcome() {
  return (
    <div className="welcome">
      <h1>Hello, world!</h1>
      <p>Welcome to React</p>
    </div>
  );
}
// What Babel produces (simplified)
function Welcome() {
  return React.createElement(
    'div',
    { className: 'welcome' },
    React.createElement('h1', null, 'Hello, world!'),
    React.createElement('p', null, 'Welcome to React')
  );
}
💡 The blueprint analogy. JSX is a blueprint for a building. The tags are rooms, the attributes are each room's specifications, and Babel is the architect who converts the blueprint into precise construction instructions. The DOM is the finished building.

JSX Syntax Rules

JSX looks like HTML but follows a few strict rules. Learn these five and most "why won't this compile" errors disappear.

1. Return a single root element

A component can only return one top-level element. Wrap siblings in a parent — or, better, in a Fragment (<>…</>) so you don't add an extra DOM node:

// ❌ Two root elements — won't compile
return (
  <h1>Title</h1>
  <p>Paragraph</p>
);

// ✅ Wrapped in a Fragment
return (
  <>
    <h1>Title</h1>
    <p>Paragraph</p>
  </>
);

2. Close every tag

Unlike forgiving HTML, JSX requires all tags to close. Void elements like <img>, <br>, and <input> must be self-closed with a trailing slash: <img src="x.jpg" alt="x" />.

3. camelCase most attributes

Because JSX becomes JavaScript, attribute names follow JS conventions. The two you'll hit most: class becomes className, and for becomes htmlFor. Event handlers are camelCase too: onclickonClick.

HTMLJSX
class="card"className="card"
for="email"htmlFor="email"
onclick="..."onClick={handleClick}
tabindex="0"tabIndex="0"
style="color:red"style={{ color: 'red' }}

data-* and aria-* attributes are the exception — they keep their hyphenated HTML form.

4. Embed JavaScript with curly braces

Anywhere inside JSX, { } drops you back into JavaScript for a single expression — a value, not a statement. More on this next.

5. Comment with {/* … */}

HTML comments don't work inside JSX. Use a JavaScript comment inside braces: {/* like this */}.

📖 style takes an object

Inline styles are a JavaScript object, not a string, and properties are camelCased: style={{ backgroundColor: 'navy', fontSize: '16px' }}. The outer braces enter JavaScript; the inner braces are the object literal. For real projects, prefer CSS classes or CSS Modules over inline styles.

Expressions & Conditionals

The power of JSX is that { } accepts any JavaScript expression: variables, math, function calls, ternaries. It does not accept statements like if, for, or switch — those go outside the JSX.

function UserGreeting({ user }) {
  const formatName = (u) => `${u.name} (${u.age})`;

  return (
    <div>
      {/* variables */}
      <h1>Hello, {user.name}!</h1>

      {/* function calls */}
      <p>Welcome back, {formatName(user)}</p>

      {/* math & string methods */}
      <p>Next year you'll be {user.age + 1}</p>
      <p>Name in caps: {user.name.toUpperCase()}</p>
    </div>
  );
}

Conditional rendering

Because JSX only takes expressions, you render conditionally with expressions too. The two idioms you'll use daily:

function Dashboard({ isLoggedIn, unread }) {
  return (
    <div>
      {/* Ternary: choose between two outputs */}
      {isLoggedIn
        ? <button>Log Out</button>
        : <button>Log In</button>}

      {/* Logical AND: render only when the condition is true */}
      {isLoggedIn && unread > 0 && (
        <p>You have {unread} unread messages</p>
      )}
    </div>
  );
}

⚠️ The 0 trap with &&

With {count && <Badge />}, if count is 0, React renders the number 0 on screen instead of nothing — because 0 is falsy but still a renderable value. Guard with a real boolean: {count > 0 && <Badge />}, or use a ternary that returns null.

Lists, Keys & Fragments

To render a collection, map an array to an array of JSX elements. Each element needs a key — a stable, unique identifier that helps React's diffing algorithm track items across re-renders.

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

⚠️ Don't use the array index as a key (usually)

Using key={index} looks convenient but breaks when the list can reorder, filter, or have items inserted — React mismatches items and you get subtle bugs (wrong input values, lost focus). Prefer a stable id from your data. An index is acceptable only for a static list that never changes order.

Fragments

A Fragment groups children without adding a wrapper DOM node. Use the short syntax <>…</> most of the time; use the explicit <Fragment key={…}> form when you need a key on the group (for example, rendering pairs in a list):

import { Fragment } from 'react';

function Glossary({ terms }) {
  return (
    <dl>
      {terms.map((t) => (
        <Fragment key={t.id}>
          <dt>{t.word}</dt>
          <dd>{t.definition}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

Fragments also matter for valid HTML: you can't wrap <td> cells or <option> elements in a <div> without breaking the table or select. A Fragment groups them cleanly.

Props in Depth

Props are how a parent passes data to a child. They arrive as a single object; destructuring in the parameter list is the standard, readable way to use them.

flowchart TD A[Parent Component] -->|"name, role, onSave"| B[Child Component] B -->|"calls onSave(data)"| A

Passing and receiving

// Parent passes props
function App() {
  const user = { name: 'Alice', role: 'Admin' };
  return <UserBadge name={user.name} role={user.role} />;
}

// Child destructures them
function UserBadge({ name, role }) {
  return <span>{name} — {role}</span>;
}

Default values

Give props defaults right in the destructuring. This replaces the old defaultProps approach for function components:

function Button({ variant = 'primary', size = 'medium', children }) {
  return <button className={`btn btn-${variant} btn-${size}`}>{children}</button>;
}

// <Button>Save</Button>                → primary, medium
// <Button variant="danger">Delete</Button> → danger, medium

The spread operator: forwarding props

When you want to pass most props straight through to an inner element, collect the rest with ...rest and spread them. This keeps wrapper components thin:

function TextInput({ label, id, ...rest }) {
  return (
    <div className="field">
      <label htmlFor={id}>{label}</label>
      {/* type, value, onChange, placeholder… all forwarded */}
      <input id={id} {...rest} />
    </div>
  );
}

// <TextInput label="Email" id="email" type="email" placeholder="you@site.com" />

The children prop

Whatever you nest between a component's tags arrives as the special children prop — the key to reusable containers:

function Panel({ title, children }) {
  return (
    <section className="panel">
      <h2>{title}</h2>
      <div>{children}</div>
    </section>
  );
}

// <Panel title="Notes"><p>Anything can go here.</p></Panel>

✅ Type-checking props

Larger projects catch prop mistakes early with TypeScript (define an interface for the props) — the modern default. Older JavaScript codebases use the prop-types package for runtime checks. Either way, documenting a component's expected props pays for itself the first time someone misuses it.

Common Pitfalls

These trip up nearly everyone at first. Recognizing them saves hours:

  • Rendering an object directly: {user} throws "Objects are not valid as a React child." Render specific fields ({user.name}) or map the object to elements.
  • Forgetting className: writing class="..." silently does the wrong thing (or warns). Always className.
  • Mutating props: props are read-only. Never assign to a prop inside a child; compute a new value instead.
  • Missing keys in lists: React warns, and updates can misbehave. Always give mapped elements a stable key.
  • The 0/NaN && trap: guard with an explicit boolean comparison as shown earlier.

Console warning you'll see for the last one:

Warning: Each child in a list should have a unique "key" prop.

Hands-on Exercise

🏋️ Build a Prop-Driven Product Card

Objective: Practice props, defaults, conditional rendering, and lists in one component.

Requirements:

  1. Create a ProductCard component that takes name, price, and inStock props.
  2. Format the price as currency with Intl.NumberFormat.
  3. Show a green "In stock" or red "Out of stock" label using conditional rendering.
  4. Give inStock a default of true.
  5. Create a ProductList that maps an array of products to ProductCards with correct keys.
const products = [
  { id: 'p1', name: 'Laptop', price: 999.99, inStock: true },
  { id: 'p2', name: 'Phone', price: 699.99, inStock: false },
  { id: 'p3', name: 'Tablet', price: 399.99 } // inStock defaults to true
];
💡 Hint

Currency: new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(price). For the label, a ternary works well: {inStock ? '✅ In stock' : '❌ Out of stock'}.

✅ Sample solution
function ProductCard({ name, price, inStock = true }) {
  const formatted = new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD'
  }).format(price);

  return (
    <article className="product-card">
      <h3>{name}</h3>
      <p>{formatted}</p>
      <p style={{ color: inStock ? 'green' : 'red' }}>
        {inStock ? '✅ In stock' : '❌ Out of stock'}
      </p>
    </article>
  );
}

function ProductList({ products }) {
  return (
    <div className="product-list">
      {products.map((p) => (
        <ProductCard
          key={p.id}
          name={p.name}
          price={p.price}
          inStock={p.inStock}
        />
      ))}
    </div>
  );
}

Best Practices

✅ Do

  • Destructure props in the parameter list — it documents what a component expects.
  • Extract complex logic into variables or helper functions above the return, keeping JSX readable.
  • Use Fragments instead of throwaway wrapper <div>s.
  • Give every mapped element a stable, unique key.
  • Prefer CSS classes over inline style objects for anything reused.

⚠️ Don't

  • Don't put if/for statements inside { } — only expressions belong there.
  • Don't render raw objects or arrays of objects as children.
  • Don't use && with a numeric left side without a boolean guard.
  • Don't reach for dangerouslySetInnerHTML unless you fully trust and sanitize the HTML.

Summary & Quiz

🎉 Key Takeaways

  • JSX is HTML-like syntax that Babel compiles to createElement calls.
  • Return one root element, close all tags, and use className/htmlFor.
  • { } embeds JavaScript expressions; conditionals use ternary or &&.
  • Render lists with .map() and stable keys; group with Fragments.
  • Props pass data down — destructure them, default them, and forward the rest with spread; nested content is children.

🎯 Quick Quiz

Question 1: Why must you write className instead of class in JSX?

Question 2: What is the purpose of the key prop when rendering a list?

Question 3: How does a component receive content nested between its opening and closing tags?

📚 Further Reading

🚀 What's Next?

You can now describe any static UI and feed it data through props. Next we make it come alive: state management with HooksuseState, useEffect, and friends — so components can remember, react, and update over time.

🎉 Nicely done!

JSX and props are the vocabulary of React. From here, everything is about behavior.