# Components and JSX

# JSX is not HTML

JSX combines HTML-like markup with JavaScript

TSX looks like markup inside TypeScript, but it is syntax for function calls: each element compiles to a call that creates a description object, not a DOM node and not an HTML string. <h1 className="title">Hello</h1> becomes roughly createElement('h1', { className: 'title' }, 'Hello').

const element = <h1 className="title">Hello</h1>;

Differences from HTML:

  • Tags starting with an uppercase letter are your components; lowercase tags are built-in DOM elements.
  • Attributes are JavaScript expressions and use JavaScript names: className (not class), htmlFor (not for).
  • {expression} embeds any JavaScript expression—values, ternaries, .map() results—but not statements like if.
  • Interpolated values are escaped by default, which prevents injecting markup from data. React provides dangerouslySetInnerHTML for raw HTML; avoid it.

# What a component may return

Adjacent JSX elements need one parent, so a return cannot contain two sibling tags. A component is not limited to a single element, though: it may return an element, an array of elements, a string, a number, null, or a fragment. Returning null (or false) renders nothing—useful for conditional UI:

function Badge({ count }: { count: number }) {
  if (count === 0) {
    return null;
  }
  return <span className="badge">{count}</span>;
}

A fragment (<>...</>) groups children without adding a DOM node, which is how you satisfy the "one parent" rule when no wrapper element belongs in the markup:

function NamePrice() {
  return (
    <>
      <td>Name</td>
      <td>Price</td>
    </>
  );
}

# Typed props and children

Props are a plain object—type it like one:

interface Product {
  id: number;
  name: string;
  price: number;
}

interface ProductRowProps {
  product: Product;
  highlighted?: boolean;
}

function ProductRow({ product, highlighted = false }: ProductRowProps) {
  return (
    <tr className={highlighted ? 'highlight' : undefined}>
      <td>{product.name}</td>
      <td>{product.price}</td>
    </tr>
  );
}

A component that wraps arbitrary content accepts children; PropsWithChildren<T> adds it to your props type:

import type { PropsWithChildren } from 'react';

function Panel({ title, children }: PropsWithChildren<{ title: string }>) {
  return (
    <section>
      <h2>{title}</h2>
      {children}
    </section>
  );
}

function Summary() {
  return (
    <Panel title="Totals">
      <p>Everything looks good.</p>
    </Panel>
  );
}

# Composition

Build screens by nesting small components instead of writing one large render. A parent passes data down as props and slots content in via children. The complete example below composes ProductRow inside ProductList.

In this small example DataRow composes a stateful child, EditButton:

import { useState } from 'react';

function EditButton() {
  const [checked, setChecked] = useState(false);
  return (
    <label>
      <input
        type="checkbox"
        checked={checked}
        onChange={(e) => setChecked(e.currentTarget.checked)}
      />
      Select product
    </label>
  );
}

interface DataRowProps {
  description: string;
  value: number;
}

function DataRow(props: DataRowProps) {
  return (
    <div className="table-row">
      <div>{props.description}</div>
      <div>$ {props.value}</div>
      <div>
        <EditButton />
      </div>
    </div>
  );
}

The parent passes each product's data as props; the same DataRow type is reused per product, and each instance's EditButton keeps its own independent local state. Because local state is encapsulated inside EditButton, the parent cannot observe which rows are selected—moving that state into the parent with a controlled component is the fix, shown there.

# Lists and keys

When rendering an array, each element needs a key on the mapped root element so React can match items across renders:

{products.map((p) => <ProductRow key={p.id} product={p} />)}

Rules that matter:

  • key is React's bookkeeping—it is not passed to the component as a prop (props.key is undefined).
  • Use a stable identity (database id), not the array index. Index keys break when the list is reordered or filtered: React matches by position and may reuse the wrong component state.
  • State lives per position + key + component type. Changing a key remounts the component and resets its state.

Using the map index works only while the list never changes order or membership:

{data.products.map((product, index) => (
  <DataRow key={index} description={product.name} value={product.cost} />
))}

The version below uses each product's stable id instead—this is the pattern to prefer:

{data.products.map((product) => (
  <DataRow key={product.id} description={product.name} value={product.cost} />
))}

With index keys, reordering makes React pair each position with a different product; a child's local state stays with the position, not the item.

# Complete example

interface Product {
  id: number;
  name: string;
  price: number;
}

function ProductRow({ product }: { product: Product }) {
  return (
    <tr>
      <td>{product.name}</td>
      <td>${product.price.toFixed(2)}</td>
    </tr>
  );
}

function ProductList({ products }: { products: Product[] }) {
  return (
    <table>
      <thead>
        <tr><th>Name</th><th>Price</th></tr>
      </thead>
      <tbody>
        {products.map((p) => <ProductRow key={p.id} product={p} />)}
      </tbody>
    </table>
  );
}

Try it: take the product array from your pet project and render it twice—once with key={p.id} and once with the map index. Put a stateful child (e.g. a text input) inside a row, reorder the array, and observe which version keeps each input's state on the right product.