Step 12 of 21
React fundamentals
Build UIs from typed React components: write JSX, pass props and children, render conditionally and render lists with keys.
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>.
// src/Greeting.tsx
export function Greeting() {
return <h1>Hello, LevelCode!</h1>
}Components are composed by using them inside each other, exactly like HTML tags:
// 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:
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:
classNameinstead ofclass,htmlForinstead offor,onClick,tabIndex. The exceptions arearia-*anddata-*, which keep their dashes. styletakes 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.
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:
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:
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:
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.
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":
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.
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:
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.
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 ReviewItems. 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:
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 },
]- A
ProductCardthat shows the name and a formatted price, plus a "Sold out" badge whenstockis 0. - A
ProductListthat takesproductsand an optionalinStockOnlyboolean prop and renders a card per product with a correct key. - When nothing matches, show "No products found." instead of an empty list.
- Render
<ProductList products={products} inStockOnly />fromApp.
Show solution
// 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>
)
}// 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;
childrenlets 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
mapand give each item a stable, uniquekey, usually the id from your data. - Rendering must be pure;
StrictModedouble-renders in development to expose violations.
Check yourself
Quiz
0/4 correct · 4 left
1.`items` is an empty array. What does `<div>{items.length && <ItemList items={items} />}</div>` render?
2.Which is the best key for rows in a list of orders loaded from your API?
3.A button is written as `<button onClick={handleDelete(order.id)}>`. What happens?
4.A child component needs to display a different label after something changes. What is the correct way to do it?
Concepts in this lesson
One-page notes on each idea, for review or a quick lookup later.
Finished reading?
Mark it complete to fill in your roadmap.