Chapters

Hide chapters

React Apprentice

First Edition · web · React 8.0.0 · Visual Studio Code

Section I: Rendering Right

Section 1: 7 chapters
Show chapters Hide chapters

3. JSX & Components
Written by Eli Ganim

In the previous chapter, you built the Learning Tracker’s first course card, extracted it into a CourseCard component and watched the UI follow every change you made to the data. Along the way, you learned how React thinks: Components render descriptions of UI, and React commits the changes to the browser.

But be honest — some of what you typed, you took on faith. Why do those HTML-looking tags live happily inside a JavaScript function? Why did <CourseCard /> need a capital C? What exactly are the braces allowed to hold? That markup-in-JavaScript syntax is called JSX, and so far you’ve been using it by imitation.

This chapter replaces imitation with understanding. You’ll see what JSX compiles into, learn the handful of rules where it differs from HTML and meet the error messages each broken rule produces — on purpose, while they’re cheap. Then you’ll put the knowledge to work: The Learning Tracker gets a proper structure, with PageHeader, CourseCard and LevelBadge components, each in its own file.

By the end, the page will look exactly like it does now. Everything underneath it will be better.

JSX Under the Hood

Here’s the uncomfortable truth about the markup you’ve been writing: The browser never sees it. Browsers speak HTML, CSS and JavaScript — JSX isn’t on the list. Before your app reaches the browser, Vite translates every scrap of JSX into plain JavaScript:

JSX is a convenience for you, not the browser — it compiles to an ordinary function call.
JSX is a convenience for you, not the browser — it compiles to an ordinary function call.

The heading from your course card, <h2>{course.title}</h2>, becomes a function call: jsx('h2', { children: course.title }). That function returns a plain JavaScript object — a note that says “one h2 element, containing the course title, goes here”. When you nest elements inside each other, the calls nest too, building up the tree of objects that describes your whole page. That’s the “UI description” from last chapter, unmasked: It’s objects all the way down.

This one fact explains most of JSX’s personality:

  • JSX is an expression. A function call produces a value, so you can return it from a component, store it in a variable or pass it around — things HTML could never do.
  • JSX follows JavaScript’s rules, not HTML’s. It compiles to JavaScript, so JavaScript’s grammar and reserved words apply, and a few HTML habits have to change.
  • Components stay in React’s hands. <CourseCard /> compiles to jsx(CourseCard, ...) — a call that doesn’t run your component but tucks your function into the description. React calls it during the render, exactly as you saw in Chapter 2.

Keep “it’s just a function call” in your back pocket. Every rule in this chapter follows from it.

Unlearning a Little HTML

JSX looks so much like HTML that your fingers will keep typing HTML. Mostly that works; in a few places it doesn’t. Time to meet the most common one in person.

In src/App.tsx, find CourseCard’s opening <article> tag and change it to:

<article class="course-card">

In HTML, class is how you’d attach a CSS class. Save, and TypeScript objects immediately — hover over the red underline and read the last line of the error:

Property 'class' does not exist on type
'DetailedHTMLProps<HTMLAttributes<HTMLElement>,
HTMLElement>'. Did you mean 'className'?

The middle of that message is a mouthful, but the first and last lines say it all: there’s no class here, did you mean className? The rename isn’t JSX being difficult. JSX takes its attribute names from the DOM — the browser’s JavaScript API for the page — rather than from HTML source. And the DOM has exposed this property as className since JavaScript’s early days, when a reserved word like class — the keyword that declares a class, as in object-oriented programming — couldn’t be used as a property name. React follows the DOM’s naming, and TypeScript holds you to it.

You won’t need CSS classes until Chapter 6, so delete the attribute, returning the tag to plain <article>. Save, and the error disappears.

A few more naming rules follow the same logic, and it’s enough to recognize them when you see them:

  • className instead of class, as you just saw.
  • htmlFor instead of for on labels — same story: for is also a reserved word, the one that starts loops, so the DOM property became htmlFor. You’ll use it when the app grows forms in Chapter 9.
  • camelCase attribute names: M.ulti-word attributes follow the DOM’s camelCase property style, so tabindex and onclick become tabIndex and onClick.
  • aria- and data- attributes keep their HTML spelling: accessibility attributes like aria-label and custom data- attributes stay dashed and lowercase, exactly as you’d write them in HTML.
  • Every tag closes. HTML forgives an unclosed <br> or <img>, but a function call can’t be left half-written, so JSX won’t. Elements without children close inline — <img /> — exactly like your <CourseCard />.

Note: You don’t need to memorize the full list. TypeScript flags every one of these the moment you type it, usually with the correct name in a “Did you mean…?” suggestion. Let the tooling be your memory.

Putting Expressions in Braces

You’ve used braces since Chapter 1 to drop values into markup. Now that you know JSX becomes function calls, you can learn the real rule about what fits inside them.

In CourseCard, add two lines at the bottom of the article, below the favorite message:

<p>{course.title.toUpperCase()}</p>
<p>Seats left: {30 - 12}</p>

Save and check the browser:

Braces evaluate whatever expression you give them and render the result.
Braces evaluate whatever expression you give them and render the result.

The card now shouts “REACT BASICS” and reports 18 seats. Neither value exists anywhere in your data — the first is a string method transforming course.title, the second is arithmetic evaluated on the spot.

The rule: Braces accept any JavaScript expression — code that produces a value. course.title, course.title.toUpperCase(), 30 - 12, a variable name, even another piece of JSX — all fair game. What appears on the page depends on the value. Strings and numbers render as text, right where the braces sit. A few values render nothing at all: true, false, null and undefined leave the page untouched — useless now, but Chapter 5 turns that quirk into a superpower. And React draws the line at whole objects: If you put {course} in your JSX, the app crashes with “Objects are not valid as a React child”.

What braces don’t accept is a statement — code that performs an action rather than producing a value. if is the classic example. Try to write {if (course.isFavorite) { ... }} in JSX, and TypeScript stops you with a terse:

Expression expected.

That error is worth remembering, because it’s exactly what you’ll see whenever a statement sneaks into braces. The fix is the pattern you already used in Chapter 2: Do your if work before the return, store the result in a variable — like favoriteMessage — and put the variable in the braces. Write statements above the return and expressions inside the JSX.

Your experiment lines have served their purpose. Delete both — the shouting title and the seat count — then save and check that the card is back to its Chapter 2 self.

Extracting the Page Header

Enough theory — time to put JSX’s rules under load. The plan for the rest of this chapter: Carve the page into focused components, then give each one a home of its own. First up — the page header.

In src/App.tsx, add this function above App:

function PageHeader() {
  return (
    <h1>Learning Tracker</h1>
    <p>Your course catalog starts here.</p>
  )
}

It’s a straight copy of the heading and intro line from App, wrapped in a function — the same move you made for CourseCard in the previous chapter. Save the file.

Red ink everywhere! In the editor, TypeScript underlines the <p> line and reports:

JSX expressions must have one parent element.

The browser is even less subtle:

Vite's error overlay: the code didn't compile, and the page won't update until it does.
Vite's error overlay: the code didn't compile, and the page won't update until it does.

This full-screen report is Vite’s error overlay. When your code won’t compile, the overlay names the error, points at the file and line — src/App.tsx:29 — and even suggests a fix. It looks alarming the first time, but it’s on your side: Read it top to bottom, fix the code, and it dismisses itself. You’ll see it again, so get comfortable with it.

Both messages are describing the same law of JSX. A return statement returns one value — that’s JavaScript, not React. After compilation, your two elements are two separate jsx(...) calls producing two separate objects. Returning two values side by side isn’t something a function can do, any more than return 1 2 would be.

The overlay’s help line names the built-in solution: Did you want a JSX fragment <>...</>? A fragment is a pair of empty tags that groups elements into a single value without creating any element of its own. Update PageHeader to use one:

function PageHeader() {
  return (
    <>
      <h1>Learning Tracker</h1>
      <p>Your course catalog starts here.</p>
    </>
  )
}

Now the function returns one value — the fragment — with two elements inside. You could get the same effect by wrapping everything in a <div>, and sometimes you’ll want that, but a div adds a real element to the page. A fragment adds nothing: It exists in your code, not in the DOM.

The errors are gone, so finish the extraction. Replace the whole App function with:

function App() {
  return (
    <main>
      <PageHeader />
      <CourseCard />
    </main>
  )
}

Save and check the browser: No visible change, which is exactly right — same page, same DOM, because the fragment left no trace. App now reads like a table of contents — a header, then a card.

Extracting the Level Badge

One more component hides inside the card. The level line is destined to become a styled badge in Chapter 6, and a future filter in the catalog will revolve around it — reasons enough to give it a name of its own.

Still in src/App.tsx, add this function above CourseCard:

function LevelBadge() {
  return <p>Level: {course.level}</p>
}

One element means one value, so there’s no fragment — and no parentheses — needed: A short return fits on a single line.

Now, in CourseCard, replace the level line — <p>Level: {course.level}</p> — with:

<LevelBadge />

Save and check the browser. Again, no visible change; the badge renders the same paragraph it always did, just from inside its own component.

Notice something slightly off, though: LevelBadge reaches straight into the shared course object, so every badge you’ll ever render shows this course’s level. The same is true of CourseCard itself. For a one-course catalog, fine — but it should feel like a limitation. It’s the exact problem Chapter 4 exists to solve.

Giving Each Component Its Own File

Your App.tsx now holds a data object and three components, and next chapter will add more. Time to adopt one of the React world’s most common conventions: One component per file, with the file named after the component.

Moving the Page Header

Create a new file in src named PageHeader.tsx, and give it these contents:

function PageHeader() {
  return (
    <>
      <h1>Learning Tracker</h1>
      <p>Your course catalog starts here.</p>
    </>
  )
}

export default PageHeader

The function is unchanged. The news is the last line: export default PageHeader declares this file’s main offering, the thing other files get when they import it.

Now, back in src/App.tsx, delete the PageHeader function and add this import at the top of the file, below the App.css line:

import PageHeader from './PageHeader.tsx'

Save and check the browser — still the same page. App uses PageHeader exactly as before; the component just commutes in from another file now.

Note: JavaScript files share code through exports and imports. A default export — at most one per file — is the file’s headline act, imported without braces: import PageHeader from './PageHeader.tsx'. The ./ means “starting from the folder this file is in”, and this project’s convention, matching what Vite generated, is to spell out the file extension. You’ve been benefiting from this system all along: Take a peek at src/main.tsx ,and you’ll find import App from './App.tsx' — your whole app enters the page through a default export.

Moving the Course Data

The course object deserves its own home too — it’s data, not UI, and two components read it. Create src/course.ts with:

export const course = {
  title: 'React Basics',
  description: 'Build interfaces from reusable components.',
  level: 'Beginner',
  isFavorite: true,
}

Two details to notice. The extension is .ts, not .tsx — there’s no JSX in this file, so it’s plain TypeScript. And the export is different: export const right in front of the declaration, with no default. This is a named export. Where a file has one headline act, default fits; for values a file might share several of — data, helper functions, constants — named exports work better, and you import them by their exact name, in braces.

Delete the course object from src/App.tsx and import it instead, below the other imports:

import { course } from './course.ts'

Save — same page, again. From here on, the book follows this split: default exports for components, named exports for everything else.

Moving the Card and the Badge

Two components to go, and they travel together, because CourseCard renders LevelBadge. Create src/LevelBadge.tsx:

import { course } from './course.ts'

function LevelBadge() {
  return <p>Level: {course.level}</p>
}

export default LevelBadge

The badge imports the data it reads — files declare their own dependencies, always. Next, create src/CourseCard.tsx:

import LevelBadge from './LevelBadge.tsx'
import { course } from './course.ts'

function CourseCard() {
  let favoriteMessage = 'Not in your favorites yet'
  if (course.isFavorite) {
    favoriteMessage = 'One of your favorites'
  }

  return (
    <article>
      <h2>{course.title}</h2>
      <p>{course.description}</p>
      <LevelBadge />
      <p>{favoriteMessage}</p>
    </article>
  )
}

export default CourseCard

Both import styles, working side by side: LevelBadge arrives as a default export, no braces; course arrives as a named export, in braces. Components importing components is how React apps assemble — App doesn’t need to know LevelBadge exists, because CourseCard handles its own suppliers.

Finally, strip src/App.tsx down to its finished form:

import './App.css'
import PageHeader from './PageHeader.tsx'
import CourseCard from './CourseCard.tsx'

function App() {
  return (
    <main>
      <PageHeader />
      <CourseCard />
    </main>
  )
}

export default App

The course and LevelBadge imports are gone from App — it no longer touches either directly. Save everything, and build and run:

Four files of surgery later: the page is pixel-for-pixel the same.
Four files of surgery later: the page is pixel-for-pixel the same.

Identical to where the chapter started — the refactoring goal, achieved. Here’s the same page as a component tree, before and after:

The DOM didn't change; the organization above it did.
The DOM didn't change; the organization above it did.

The dashed boxes — the real elements — are the same in both trees. What changed is the solid boxes: One big component became four focused ones, each with a name, a job and a file you can find in a second flat.

The conventions you just followed are worth stating once, plainly, because the whole book sticks to them — and you’ll recognize them in plenty of React codebases:

  • Component names are PascalCase: every word capitalized, like PageHeader.
  • Each component lives in a file named after it: PageHeader in PageHeader.tsx.
  • Files containing JSX use the .tsx extension; plain logic and data use .ts.
  • Components are default exports; data and helpers are named exports.

Note: Look at how little TypeScript ceremony this took: none. You didn’t annotate a single component — no special “component type” exists in this book because none is needed. A component is a plain function, and TypeScript follows the plot across files: Hover over course inside CourseCard.tsx, and there’s the full shape, inferred in course.ts and carried through the import. If you meet code online that types components with React.FC, that’s a style choice some codebases make, not a requirement — this book sticks with plain functions and lets inference do the work.

Decoding JSX Error Messages

You now know JSX’s rules well enough to break them productively. Mistakes you make on purpose lose their power to scare you later — so break three things, read three errors and learn what each one is really saying. Sabotage time.

Sabotage #1: the lowercase component. In src/App.tsx, change <PageHeader /> to <pageHeader /> and save. TypeScript reports:

Property 'pageHeader' does not exist on type
'JSX.IntrinsicElements'.

JSX.IntrinsicElements is TypeScript’s name for the list of built-in HTML tags — remember from Chapter 2 that a lowercase tag means “HTML element”, so TypeScript went looking for an element called pageHeader and found nothing. As a bonus, a second error appears on the import line: 'PageHeader' is declared but its value is never read. Your component is sitting in the file, imported and ignored. When you see this pair together, the diagnosis is almost always a miscapitalized tag. Fix the capital P.

Sabotage #2: the unclosed tag. In src/PageHeader.tsx, delete the closing </p> from the intro line and save. The first error says:

JSX fragment has no corresponding closing tag.

Wait — the fragment? You broke the paragraph. But with no </p> in sight, TypeScript assumes everything after <p> is still inside the paragraph, including your closing </>. So the fragment appears unclosed, and the complaint lands two lines above the actual crime. This is the single most disorienting thing about JSX errors: One missing closing tag makes everything after it look broken. When an error insists a tag you can plainly see is missing, don’t trust the line number — scan upward for an earlier tag that never closed. Restore the </p>.

Sabotage #3: the mistyped import. In src/App.tsx, change the CourseCard import path to './CourseCrad.tsx' and save:

Cannot find module './CourseCrad.tsx' or its
corresponding type declarations.

This one means exactly what it says: No file matches the path. Check the spelling, check the folder, check the extension. You’ll meet it most often right after creating a new file, when your fingers and your file names haven’t agreed yet. Fix the path, save and confirm the app compiles cleanly — your sabotage spree is over, and the code is back to its finished state.

Challenge: Extract the Catalog Intro

Practice the full extraction ritual solo, end to end. Take the intro line — <p>Your course catalog starts here.</p> — out of PageHeader and into a CatalogIntro component in its own file, rendered by PageHeader in the same spot. The page shouldn’t change at all.

A few hints:

  • The moves are exactly the ones from this chapter: Create src/CatalogIntro.tsx, write the function, export it, import it and use it.
  • The component returns a single element, so no fragment — and no parentheses — required.
  • PageHeader keeps its own fragment: It still returns the heading and one other thing.

You’ll find a complete solution in the challenge folder of this chapter’s materials.

Key Points

  • JSX compiles to JavaScript function calls — the browser never sees it, and each call returns an object describing one element.
  • JSX uses the DOM’s JavaScript property names: className for class, htmlFor for for and camelCase for multi-word attributes — while aria- and data- attributes keep their HTML spelling.
  • In JSX, every tag closes, including self-closing ones like <img />.
  • Braces hold any expression — strings and numbers render as text; booleans, null and undefined render nothing. Statements like if stay above the return, feeding variables into the braces.
  • A component returns one root value; a fragment (<>...</>) groups siblings without adding anything to the DOM.
  • Vite’s error overlay reports compile errors in the browser — read it, fix it, and it goes away by itself.
  • Organize with one component per file, PascalCase names, .tsx for JSX files and .ts for the rest.
  • Default exports carry components; named exports carry data and helpers. Files import what they use, by relative path.
  • Components need no special TypeScript type — they’re plain functions, and inference follows your data across files.
  • A missing closing tag breaks everything after it — when an error seems impossible, look above the reported line.

Where to Go From Here?

The Learning Tracker now has real architecture: four components with clear jobs, each in a findable file. But you saw the crack in the foundation — CourseCard and LevelBadge read one shared course object, so they can only ever show one course. A catalog of identical cards isn’t much of a catalog.

What components need is a way to receive data from the outside, the way functions receive arguments. React calls them props, and they’re the subject of the next chapter — where your card finally learns to display any course you hand it.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.