AnyLearn
All lessons
Programmingintermediate

React Components and JSX

Master React 19's core primitive: function components, JSX, props, children, composition, conditional rendering, and stable keys — built around a small TODO list you can ship.

Not signed in — your progress and quiz score won't be saved.
Lesson progress1 / 10

JSX is just function calls

JSX is sugar. Your bundler rewrites it into React.createElement (or, in the React 19 JSX transform, jsx/jsxs from react/jsx-runtime). The runtime is the same call: tag name + props object + children.

const el = <button className="primary" onClick={save}>Save</button>;
// becomes (roughly):
const el = jsx('button', { className: 'primary', onClick: save, children: 'Save' });

That is the whole magic. JSX is HTML-shaped JavaScript. Curly braces drop back into JS, attributes are camelCase (className, htmlFor, tabIndex), and the value is an element — a plain object describing what to render. React's job is to take that object tree and reconcile it against the DOM.

Full lesson text

All 10 steps on one page — for reading, reference, and search.

Show

1. JSX is just function calls

JSX is sugar. Your bundler rewrites it into React.createElement (or, in the React 19 JSX transform, jsx/jsxs from react/jsx-runtime). The runtime is the same call: tag name + props object + children.

const el = <button className="primary" onClick={save}>Save</button>;
// becomes (roughly):
const el = jsx('button', { className: 'primary', onClick: save, children: 'Save' });

That is the whole magic. JSX is HTML-shaped JavaScript. Curly braces drop back into JS, attributes are camelCase (className, htmlFor, tabIndex), and the value is an element — a plain object describing what to render. React's job is to take that object tree and reconcile it against the DOM.

2. Components are functions returning JSX

In React 19, components are JS functions. They take a props object and return JSX (or null, a string, a number, an array, a Promise — but mostly JSX).

type TodoProps = { title: string; done: boolean };

function Todo({ title, done }: TodoProps) {
  return (
    <li className={done ? 'done' : ''}>
      <input type="checkbox" defaultChecked={done} /> {title}
    </li>
  );
}

Class components still work but you should not be writing them in 2026. Function components plus hooks cover every case. Name components in PascalCase — that is how JSX distinguishes <Todo /> (your component) from <todo /> (a DOM element).

3. Props are immutable inputs

Props flow one way: parent passes them, child reads them. Inside a component, never mutate props. If you find yourself wanting to, you actually want state (covered next lesson) or you want to lift the data up and have the parent pass new props.

function Counter({ value }: { value: number }) {
  // value++  ❌ mutating a prop
  return <span>{value}</span>;
}

This matters because React reuses your function. The same component renders many times with different props — pure read-only inputs make that predictable. Strict Mode (on by default in create-next-app / Vite templates) will actually invoke your component twice in development to surface mutation bugs.

4. children and composition

children is the prop that holds whatever you put between your component's opening and closing tag. It is how React does composition — the React idiom that replaces inheritance.

function Card({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <section className="card">
      <h3>{title}</h3>
      <div className="card-body">{children}</div>
    </section>
  );
}

// usage
<Card title="Today">
  <TodoList />
</Card>

A Card doesn't need to know what a TodoList is. It just renders whatever children it's given. This is how every well-aged React component looks: small, unaware of its concrete contents.

5. Conditional rendering

Three idioms cover almost everything:

// 1. ternary for either/or
{isLoggedIn ? <Profile /> : <SignIn />}

// 2. && for show/hide
{hasError && <Banner message={error} />}

// 3. early return for whole-component branches
if (loading) return <Spinner />;

Gotcha: 0 && <X /> renders 0. Avoid by using Boolean(count) && <X /> or count > 0 && <X /> when the left side could be a falsy number. Strings work the same way — '' && <X /> is fine because empty string isn't rendered, but '0' is.

6. Rendering the TODO tree

The TODO app composes small components. Data flows down via props; events bubble up via callbacks.

flowchart TD
  A["App"] --> B["Header"]
  A --> C["TodoList"]
  C --> D["TodoItem (id: 1)"]
  C --> E["TodoItem (id: 2)"]
  C --> F["TodoItem (id: 3)"]
  A --> G["AddTodoForm"]

7. Lists and the keys rule

When you render an array of elements, React needs a stable identity for each item so it can match new lists against old. That identity is the key prop.

function TodoList({ todos }: { todos: { id: string; title: string }[] }) {
  return (
    <ul>
      {todos.map(t => <Todo key={t.id} title={t.title} />)}
    </ul>
  );
}

Rules: keys must be stable (don't regenerate per render), unique among siblings (not globally), and predictable (not random). The classic anti-pattern is key={index} — fine for a static list, broken the moment you reorder, delete, or insert in the middle, because React will reuse the wrong DOM nodes and state.

8. Controlled vs uncontrolled inputs (preview)

Two ways to wire a form input:

  • Uncontrolled: the DOM owns the value. You read it on submit via a ref or FormData. Use defaultValue / defaultChecked.
  • Controlled: React state owns the value. Every keystroke triggers a re-render. Use value + onChange.
// uncontrolled
<input name="title" defaultValue="" />

// controlled (we'll cover useState next lesson)
<input value={title} onChange={e => setTitle(e.target.value)} />

In 2026, with React 19's <form action={...}> and Server Actions, uncontrolled forms are getting more popular: less re-rendering, less code, FormData is great. Controlled is still right when you need instant validation, masking, or live preview.

9. Composition beats inheritance

React deliberately has no extends Component story. Reuse comes from composition: small components that take other components as children or as named slot props.

function Dialog({ title, footer, children }: {
  title: React.ReactNode;
  footer?: React.ReactNode;
  children: React.ReactNode;
}) {
  return (
    <div className="dialog">
      <header>{title}</header>
      <main>{children}</main>
      {footer && <footer>{footer}</footer>}
    </div>
  );
}

If you find yourself reaching for extends, you almost always want either (a) a wrapper component, (b) a custom hook, or (c) a render-prop / children-as-function slot. Inheritance hierarchies in UI age badly; composed components don't.

10. What you can return from a component

React 19 widened the set of valid return values from a component. All of these are legal:

  • JSX: return <ul>...</ul>;
  • A fragment: return <>...</>;
  • null or undefined: render nothing.
  • A string or number: rendered as text.
  • An array: rendered in order (give each element a key).
  • A Promise: only in Server Components — React will await it before painting.
function Maybe({ show }: { show: boolean }) {
  if (!show) return null;
  return <span>hi</span>;
}

Fragments are the unsung hero: when you need to return siblings without a wrapping <div>, reach for <>...</>. Less DOM noise, fewer CSS surprises.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. What does <Button onClick={save}>OK</Button> get compiled into?
    • jsx('Button', { onClick: save, children: 'OK' })
    • new Button({ onClick: save }).render('OK')
    • React.render(Button, save, 'OK')
    • Button.compile({ click: save, text: 'OK' })
  2. Why is key={index} usually wrong for a list of editable TODOs?
    • Reordering or deleting items reuses the wrong DOM nodes, scrambling state and inputs.
    • It's a TypeScript error to pass a number as a key.
    • Indexes can collide with another sibling list elsewhere in the tree.
    • React forbids non-string keys.
  3. A parent passes `value` as a prop. The child wants to bump it on a button click. What's the React way?
    • Let the parent own the state and pass an onChange/onIncrement callback down.
    • Mutate props.value directly inside the child handler.
    • Reassign the imported prop in a module-scope variable.
    • Use React.cloneElement to mint a new prop value.
  4. You render `{count && <Pill>{count}</Pill>}` and when count is 0 the screen shows a stray '0'. Why?
    • 0 is falsy but React still renders the number 0 from the && expression.
    • Pill components require a positive prop or React throws.
    • && short-circuits to undefined which renders as '0'.
    • JSX coerces booleans to numbers when rendering.
  5. Which is the strongest argument FOR composition over a class hierarchy in React?
    • Components naturally accept children/slots, so reuse stays flat and testable.
    • Class inheritance forces dynamic dispatch that's slower at runtime.
    • JSX literally cannot represent an extends relationship.
    • TypeScript can't type abstract React classes.

Related lessons