# React fundamentals

> Build UIs from typed React components: write JSX, pass props and children, render conditionally and render lists with keys.

Frontend Developer roadmap · TypeScript & React · 16 min read · https://levelcode.org/roadmaps/frontend/react-fundamentals

React lets you describe your UI as a tree of **components**: small functions that take data and return what the screen should look like. Instead of writing step-by-step DOM updates, you say "given this data, the page looks like this", and React works out the DOM changes. This lesson covers components, JSX, props, conditional rendering and lists. State and effects, the parts that make components interactive over time, come in the next lesson.

## Components are functions

A React component is a JavaScript function that returns markup. Its name must start with a capital letter; that is how JSX tells your components apart from built-in tags like `<div>`.

```tsx
// src/Greeting.tsx
export function Greeting() {
  return <h1>Hello, LevelCode!</h1>
}
```

Components are composed by using them inside each other, exactly like HTML tags:

```tsx
// src/App.tsx
import { Greeting } from './Greeting'

export function App() {
  return (
    <main>
      <Greeting />
      <p>Welcome back.</p>
    </main>
  )
}
```

Something has to put the root component on the page. In a Vite project that happens once, in `src/main.tsx`:

```tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './App'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)
```

`createRoot` takes over the `<div id="root">` in `index.html`. `StrictMode` adds extra development-only checks, which you will meet again below. Everything else in your app is components rendering components.

## JSX: markup inside JavaScript

JSX looks like HTML, but it compiles to JavaScript function calls. `<h1 className="title">Hi</h1>` becomes, roughly, `jsx('h1', { className: 'title', children: 'Hi' })`, which produces a plain object describing the element. Because it is JavaScript, a few rules differ from HTML:

- **Return a single root.** Wrap siblings in an element or in a fragment, `<>...</>`, which adds nothing to the DOM.
- **Close every tag**, including void ones: `<img />`, `<input />`, `<br />`.
- **Attributes are camelCase DOM properties**: `className` instead of `class`, `htmlFor` instead of `for`, `onClick`, `tabIndex`. The exceptions are `aria-*` and `data-*`, which keep their dashes.
- **`style` takes an object**: `style={{ marginTop: 8 }}`. A bare number means pixels.

Curly braces switch from markup back into JavaScript. Inside them you can put any **expression**: variables, function calls, arithmetic, ternaries. You cannot put statements such as `if` or `for`.

```tsx
const user = { name: 'Ada', avatarUrl: '/ada.png' }

export function Profile() {
  return (
    <>
      <img src={user.avatarUrl} alt="" className="avatar" />
      <h2>{user.name.toUpperCase()}</h2>
      <p>Member for {new Date().getFullYear() - 2019} years</p>
    </>
  )
}
```

React escapes everything you put in braces, so a string containing `<script>` is displayed as text instead of being run. That protects you from most cross-site scripting bugs by default.

## Props: passing data down

Components receive data through **props**, a single object argument. In TypeScript you describe it with a type and destructure it in the parameter list, giving defaults where it makes sense:

```tsx
type BadgeProps = {
  label: string
  tone?: 'neutral' | 'success' | 'danger'
}

export function Badge({ label, tone = 'neutral' }: BadgeProps) {
  return <span className={`badge badge-${tone}`}>{label}</span>
}

// Usage: <Badge label="New" tone="success" /> and <Badge label="Draft" />
```

Strings can be passed in quotes; everything else (numbers, booleans, objects, functions) goes in braces: `count={3}`, `disabled={true}` or just `disabled`.

Props are **read-only**. A component must never modify its props object. Data flows one way, from parent to child: if something needs to change, the parent renders the child again with new props.

### Children

Whatever you nest between a component's tags arrives as the special `children` prop. This is how you build wrappers like cards, layouts and dialogs:

```tsx
import type { ReactNode } from 'react'

type CardProps = { title: string; children: ReactNode }

export function Card({ title, children }: CardProps) {
  return (
    <section className="card">
      <h2>{title}</h2>
      {children}
    </section>
  )
}

<Card title="Billing">
  <p>Your next invoice is due on 1 March.</p>
</Card>
```

`ReactNode` is the type for "anything React can render": elements, strings, numbers, arrays of those, `null`, `undefined` and booleans.

### Passing functions as props

A parent tells a child what to do on an event by passing a function. Pass the function itself; do not call it:

```tsx
type DeleteButtonProps = { onDelete: () => void }

function DeleteButton({ onDelete }: DeleteButtonProps) {
  return <button type="button" onClick={onDelete}>Delete</button>
}

function Row({ id, remove }: { id: string; remove: (id: string) => void }) {
  return <DeleteButton onDelete={() => remove(id)} />
}
```

`onClick={remove(id)}` would call `remove` **during rendering**, deleting the item as soon as it appears. The arrow function `() => remove(id)` creates a function that runs later, when the click happens.

> [!NOTE]
> In React 19, `ref` is an ordinary prop for function components. You no longer need `forwardRef` to let a parent reach a DOM node inside your component.

## Conditional rendering

Since JSX is just values, you choose what to render with ordinary JavaScript. Use an early `return` for whole alternatives, a ternary for either/or inside markup, and `&&` for "this or nothing":

```tsx
type InboxProps = { unread: number; loggedIn: boolean }

export function Inbox({ unread, loggedIn }: InboxProps) {
  if (!loggedIn) {
    return <p>Please sign in.</p>
  }

  return (
    <div>
      <h2>{unread > 0 ? `Inbox (${unread})` : 'Inbox'}</h2>
      {unread > 0 && <p>You have new messages.</p>}
    </div>
  )
}
```

Returning `null` renders nothing. `false`, `true`, `null` and `undefined` inside JSX also render nothing, but numbers do render, including `0`.

> [!WARNING]
> `{messages.length && <List items={messages} />}` renders a stray **0** on the page when the array is empty, because `0 && x` evaluates to `0`. Make the left side a real boolean: `{messages.length > 0 && ...}`.

## Rendering lists

To render a collection, transform the data array into an array of elements with `map`. You can `filter` first, since it is all plain JavaScript:

```tsx
type Task = { id: string; title: string; done: boolean }

export function TaskList({ tasks }: { tasks: Task[] }) {
  const open = tasks.filter((task) => !task.done)

  if (open.length === 0) return <p>All done!</p>

  return (
    <ul>
      {open.map((task) => (
        <li key={task.id}>{task.title}</li>
      ))}
    </ul>
  )
}
```

### Keys

Every element in a list needs a `key` that is unique among its siblings. React uses keys to match items between renders: when the list changes, it knows which item moved, which was inserted and which was removed, and it keeps each item's DOM and component state attached to the right one.

| Key choice | Verdict |
| --- | --- |
| `task.id` from your data | Correct: stable and unique |
| Array index `i` | Only acceptable for static lists that never reorder, insert or delete |
| `Math.random()` or `crypto.randomUUID()` during render | Wrong: a new key every render recreates every item and loses its state |

With index keys, deleting the first item shifts every index. React then thinks the *last* item was removed and that every other item changed, so state such as a half-typed input in a row ends up attached to the wrong task. Keys only need to be unique within one list, and `key` is consumed by React: the component does not receive it as a prop, so pass the id separately if the child needs it.

## Rendering must be pure

React may call your component function whenever it needs to work out what the UI looks like, and it may call it more than once. So a component must behave like a pure function of its props: same inputs, same JSX, no side effects while rendering.

```tsx
let renderCount = 0

function Impure() {
  renderCount++                       // mutates something outside: wrong
  return <p>Rendered {renderCount} times</p>
}
```

Do not mutate variables or objects that existed before the render, and do not change the DOM, start requests or set timers in the function body. Side effects belong in event handlers, or in effects, which the next lesson covers. Creating and modifying *new* local variables inside the render is fine.

In development, `StrictMode` calls each component function twice to surface impurities like the one above: if the output differs between the calls, you have a bug. It does not do this in production.

### Thinking in components

When you get a design, draw boxes around its parts. Each box that has a single job, or that repeats, is a candidate component. A product page might be `ProductPage` containing `ProductGallery`, `ProductInfo` and a `ReviewList` of `ReviewItem`s. Decide what data each needs, pass it down as props, and keep each component small enough to understand at a glance.

## Practice

In a Vite `react-ts` project, build a product catalogue from this data:

```ts
export type Product = { id: string; name: string; priceCents: number; stock: number }

export const products: Product[] = [
  { id: 'p1', name: 'Desk lamp', priceCents: 3900, stock: 4 },
  { id: 'p2', name: 'Notebook', priceCents: 650, stock: 0 },
  { id: 'p3', name: 'Fountain pen', priceCents: 2400, stock: 12 },
]
```

1. A `ProductCard` that shows the name and a formatted price, plus a "Sold out" badge when `stock` is 0.
2. A `ProductList` that takes `products` and an optional `inStockOnly` boolean prop and renders a card per product with a correct key.
3. When nothing matches, show "No products found." instead of an empty list.
4. Render `<ProductList products={products} inStockOnly />` from `App`.

**Solution**

```tsx
// src/ProductList.tsx
import type { Product } from './products'

const price = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })

function ProductCard({ product }: { product: Product }) {
  const soldOut = product.stock === 0
  return (
    <article className="card">
      <h3>{product.name}</h3>
      <p>{price.format(product.priceCents / 100)}</p>
      {soldOut && <span className="badge">Sold out</span>}
    </article>
  )
}

type ProductListProps = { products: Product[]; inStockOnly?: boolean }

export function ProductList({ products, inStockOnly = false }: ProductListProps) {
  const visible = inStockOnly ? products.filter((p) => p.stock > 0) : products

  if (visible.length === 0) {
    return <p>No products found.</p>
  }

  return (
    <div className="grid">
      {visible.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}
```

```tsx
// src/App.tsx
import { ProductList } from './ProductList'
import { products } from './products'

export function App() {
  return (
    <main>
      <h1>Catalogue</h1>
      <ProductList products={products} inStockOnly />
    </main>
  )
}
```

With `inStockOnly`, the page shows the lamp ($39.00) and the pen ($24.00). Notice that the filtering happens during rendering, that `soldOut` is a real boolean so `&&` is safe, and that the key goes on `ProductCard`, the element returned directly from `map`.

## Key takeaways

- A component is a capitalised function that returns JSX; you build UIs by composing components.
- JSX compiles to JavaScript: one root, closed tags, camelCase attributes and expressions in braces.
- Props flow down and are read-only; `children` lets components wrap other content, and functions passed as props let children report events.
- Use early returns, ternaries and `&&` for conditions, and make sure the left side of `&&` is a boolean.
- Render lists with `map` and give each item a stable, unique `key`, usually the id from your data.
- Rendering must be pure; `StrictMode` double-renders in development to expose violations.

## Quiz

1. `items` is an empty array. What does `<div>{items.length && <ItemList items={items} />}</div>` render?

   A. An empty div
   B. A div containing the text 0
   C. An ItemList with no items
   D. It throws an error

   **Answer: B.** `0 && anything` evaluates to `0`, and React renders numbers, so a 0 appears. Use `items.length > 0 &&` to get a boolean, which React does not render.

2. Which is the best key for rows in a list of orders loaded from your API?

   A. The array index from map
   B. crypto.randomUUID() called inside map
   C. The order's text description
   D. The order's id from the database

   **Answer: D.** Keys must be stable across renders and unique among siblings. A database id is both. Index keys break when items are inserted, removed or reordered; random keys change every render, so React recreates every row; descriptions may not be unique.

3. A button is written as `<button onClick={handleDelete(order.id)}>`. What happens?

   A. handleDelete runs during every render, and its return value becomes the click handler
   B. handleDelete runs once, on the first click
   C. React throws an error at compile time
   D. It works the same as `onClick={() => handleDelete(order.id)}`

   **Answer: A.** The braces contain a function call, so it executes while rendering. Wrap it in an arrow function so a new function is passed and called only on click.

4. A child component needs to display a different label after something changes. What is the correct way to do it?

   A. The child assigns a new value to `props.label`
   B. The child mutates a module-level variable and re-renders
   C. The parent renders the child again with a new `label` prop
   D. The child edits the DOM node directly with `document.querySelector`

   **Answer: C.** Props are read-only and data flows from parent to child. The owner of the data passes a new value down, and React updates the DOM to match.
