Lesson 1 · Basics
A quick review of the basics
JSX, components, props, children, conditions, lists and why key matters.
Loading lesson…
Lesson 1 · Basics
JSX, components, props, children, conditions, lists and why key matters.
Loading lesson…
This lesson is a quick review of the basics. Even if you know them a little, go through it – it introduces ideas the whole course builds on. And the key demo at the end catches out experienced developers too.
Picture a restaurant. A guest doesn't order with instructions ("heat the pan, add butter, crack two eggs…"). They say what they want: "a ham omelette". How it's made is the kitchen's business.
That's exactly how React works. You don't tell it how to change the page ("find this paragraph, rewrite its text, add a class"). You describe what the page should look like, and React makes sure it looks like that. This approach is called declarative: you describe the result, not the steps.
There's one omelette recipe, but hundreds of omelettes get made – each a little different: with cheese, without onion, a big portion. So the recipe expects the guest's wishes.
The same order should always produce the same dish. And the cook doesn't rewrite the order – they can't change what the guest asked for. These are the two most important rules of components, and you'll see them in code in a moment.
A big menu is made of smaller recipes: "the lunch menu" = soup + main course + dessert. An app is made of components in the same way: a page contains a header, a list and a footer, the list contains items, an item contains a button. Each component handles only its own piece, and they fit into each other like building blocks.
The analogy is nice, but what does it look like in code? We'll go through it in small steps – from plain HTML to the first component. We won't skip a single step.
This is what a greeting on a page looks like in pure HTML:
<section class="welcome"><h1>Hello, Anna!</h1><p>Good morning</p></section>
It's a hard-coded finished dish. Always the same – for Anna, in the morning. If we wanted to greet Peter in the afternoon, we'd have to write the whole HTML again.
Without React you'd change the page with JavaScript step by step – exactly like a guest dictating instructions to the cook:
const section = document.createElement('section') // take a panconst h1 = document.createElement('h1') // crack an eggh1.textContent = 'Hello, Anna!'section.append(h1)document.body.append(section)// …and when the name changes, you have to remember where h1 is and rewrite it yourself:h1.textContent = 'Hello, Peter!'
For one heading it's fine. For a whole app you quickly get lost: what all has to be touched when something changes?
React turns this around. Instead of instructions you write a function that returns what a piece of the page should look like. The HTML moves inside JavaScript – this notation is called JSX:
function Welcome() {return (<section className="welcome"><h1>Hello, Anna!</h1><p>Good morning</p></section>)}
function Welcome() is the recipe – an ordinary function. Its name starts with a capital letter.return ( … ) returns a description of the dish: what the result should look like. Nothing is rendered here yet.className instead of class (JavaScript has the word class taken).So far the recipe only knows Anna. Let's give it an order – a parameter with the name. Inside curly braces { } you can then put a value from JavaScript into JSX:
function Welcome({ name }) {return (<section className="welcome"><h1>Hello, {name}!</h1></section>)}// One recipe, two orders → two dishes<Welcome name="Anna" /><Welcome name="Peter" />
<Welcome name="Anna" /> looks like an HTML tag, and that's how the component is used. The name attribute is passed to it as the order – in React this is called props (in detail in section 2).
A recipe cooks nothing by itself. Somewhere at the start of the app (in the main.tsx file) there's a single line that tells React: "here's the main recipe, cook it into this spot on the page":
createRoot(document.getElementById('root')).render(<App />)
From then on React does the cooking. It calls App, which contains other components (say Welcome), React calls those too – and builds the page from their descriptions. When something changes later, it calls the recipes again and updates only what really differs on the page. It writes the instructions from step 2 for you.
| In the kitchen | In React | In code |
|---|---|---|
| a recipe | a component | function Welcome() { … } |
| an order | props | <Welcome name="Anna" /> |
| the finished dish | JSX – a description of the result | return <h1>Hello, {name}!</h1> |
| the cook | React | render(<App />) |
| a menu made of dishes | a component tree | <App> → <Welcome> → … |
What you see: Two cards rendered by the same Welcome component. The left one gets its order from the fields, the right one has a fixed order (Peter, afternoon).
Try it:
Welcome is an ordinary function that returns JSX.The takeaway: A component is a recipe: it describes what the result should look like for a given order. The cooking – and the re-cooking after every change – is React's job.
Loading the interactive part…
Now you know what a component is and what it's for. In section 1 we'll look at what JSX actually is and what rules it has.
The JSX from step 3 looks like HTML written in the middle of JavaScript. In reality it's just a more convenient way of writing a function call. Before it runs, it's compiled into ordinary JavaScript that creates an object describing the element:
// You write this:<Button color="blue">Save</Button>// And this is what comes out of it:jsx(Button, { color: 'blue', children: 'Save' })// → { type: Button, props: { color: 'blue', children: 'Save' } }
A component returns exactly such a description. It doesn't draw anything itself – it returns an "order for the screen" and React does the rendering. A typical first component:
function Welcome() {const name = 'Anna'const hour = new Date().getHours()return (<section className="welcome"><h1>Hello, {name}!</h1><p>{hour < 12 ? 'Good morning' : 'Good afternoon'}</p></section>)}// Usage – a component is written as a tag:<Welcome />
Let's go through what's important about it:
<welcome /> would be treated as an unknown HTML element.{…} there can be any expression – a variable, a calculation, a function call, a ternary. Statements like if or for don't fit there, though; they belong above the return.<>…</>.className instead of class, htmlFor instead of for, events in camelCase (onClick) and styles as an object (style={{ color: 'red' }}).The Welcome component above can only greet Anna. That's like a recipe for "an omelette for Anna". We want a recipe that greets anyone – the name has to come from outside. That's what props are for.
Props are simply the function's arguments. React collects all the attributes you write on the component's tag into one object and passes it as the first parameter.
interface ProfileCardProps {name: stringrole?: string // question mark = optionalavatar?: string}function ProfileCard({ name, role = 'Developer', avatar = '🙂' }: ProfileCardProps) {return (<div className="card"><span>{avatar}</span><strong>{name}</strong><div>{role}</div></div>)}// Two different orders, one recipe:<ProfileCard name="Anna Smith" role="Frontend" avatar="👩💻" /><ProfileCard name="Peter Brown" />
What's going on here:
{ name: 'Anna Smith', role: 'Frontend', avatar: '👩💻' } and calls ProfileCard with it.name, role and avatar.role nor avatar. The default values written after the equals sign are used – just like in an ordinary JS function.Some recipes make only the wrapper and leave the filling to the guest. The baker bakes a tortilla and you say what goes in it. The baker doesn't need to know every possible filling – only how to wrap a tortilla.
In React, that filling is the special prop children. It contains everything you write between the component's opening and closing tag. The component then just decides where to put that content.
function Card({ title, children }: { title: string; children: React.ReactNode }) {return (<div className="card"><h3>{title}</h3><div className="card-body">{children}</div></div>)}// The same card, a different filling each time:<Card title="News"><p>React 19 is out!</p></Card><Card title="Team"><ProfileCard name="Anna" /><ProfileCard name="Peter" /></Card>
Card knows nothing about news or the team. It only handles the frame and the heading. Thanks to that you can use it anywhere – this kind of putting together is called composition. The type React.ReactNode means "anything that can be rendered": text, a number, an element, an array of elements, or nothing.
When a component needs several places for content (a header, a body, a footer), you pass more props of type ReactNode. Such places are called slots:
<Paneltitle="Tasks"actions={<button>+ Add</button>} // a slot in the headerfooter={<small>3 tasks</small>} // a slot in the footer><TaskList /> // children = the main content</Panel>
What you see: Two cards rendered by the same ProfileCard component. The first got all the props plus content between the tags (children). The second got only name.
Try it:
children – the component just put it in the prepared spot.{children && …}. Thanks to it, the second card, which has no filling, doesn't even render an empty wrapper.The takeaway: One recipe, different orders. Props change the details, children supplies a whole piece of content, and the component doesn't need to know what's inside.
Loading the interactive part…
A recipe often contains conditions: "if the guest is vegetarian, leave out the ham", "if we're out of eggs, offer pancakes". In JSX conditions are written in ordinary JavaScript – React has no special syntax. You just have to pick the right form for the situation.
A typical example: an inbox icon with the number of new messages.
function Inbox({ user, count }: { user: User | null; count: number }) {// 1) Early return: when they're not signed in, the rest of the recipe doesn't concern usif (!user) return <p>Please sign in.</p>return (<div>{/* 2) && – either something, or nothing */}{count > 0 && <span className="badge">{count}</span>}{/* 3) a ternary – one of two versions */}<p>{count > 0 ? 'You have new messages' : 'Nothing new'}</p></div>)}
| Syntax | When to use it |
|---|---|
if (!data) return … | Loading, errors, empty states – the component ends early. The most readable. |
cond && <X/> | Either something, or nothing. The condition must be a boolean! |
cond ? <A/> : <B/> | Two versions of the UI. |
const map = { a: <A/>, b: <B/> } | Several versions by a value (instead of a chain of ternaries). |
Why the note "must be a boolean"? Let's try writing the shorter count && … instead of count > 0 && …:
{count && <span className="badge">{count}</span>}
&& operator doesn't return true/false. It returns the left side if it's "falsy", otherwise the right side.count is 3, the left side is truthy → the result is the <span>. Fine.count is 0, the left side is falsy → the result is the number 0.false, null or undefined. But it does render numbers – so a lonely "0" appears on the page.The fix: always put a real boolean into the condition – count > 0, items.length > 0, !!value.
What you see: At the top, three rows that show the number of messages in three ways: wrong (count &&), right (count > 0 &&) and with a ternary. At the bottom, a user badge written with an early return.
Try it:
count is 0. In the "Wrong" row you'll see a lonely 0, the "Right" row is empty.UserBadge component stops right on its first line and doesn't deal with the rest at all.The takeaway: Only a boolean belongs in &&. For two versions use a ternary; for "draw nothing / loading / error" an early return.
Loading the interactive part…
Picture the cloakroom in a theatre. You hand in your coat and get a ticket with a number. After the show you get your coat back by the number – even if the attendant has rearranged the hangers in the meantime.
What if the cloakroom worked by position instead of numbers: "your coat is the third from the left"? As long as nothing moves, it works. But as soon as someone hangs a coat at the start of the row, everything shifts and you leave in someone else's coat.
That's exactly what key solves in React. When you render a list, React has to remember between renders which element is which – mainly because elements can have their own state (the text in an input, an expanded detail). key is that numbered ticket.
const tasks = [{ id: 17, label: 'Go shopping' },{ id: 42, label: 'Tidy up' },]<ul>{tasks.map((task) => (<TaskRow key={task.id} task={task} />))}</ul>
map: you turn an array of data into an array of elements. (How map, filter and the other array methods work is explained in lesson 0.4.)key goes on the element that map returns. It must be unique among siblings and stable – the same item must have the same key on every render.Let's say every row has its own input for a note. In the "Go shopping" row you type "milk". Then you add a new task "Walk the dog" to the start. React compares the old and new rows by key:
| Key | Before adding | After adding | Where the note "milk" is |
|---|---|---|---|
key={index} | 0 = Go shopping, 1 = Tidy up | 0 = Walk the dog, 1 = Go shopping, 2 = Tidy up | at "Walk the dog" – row 0 kept its state |
key={task.id} | 17 = Go shopping, 42 = Tidy up | 99 = Walk the dog, 17 = Go shopping, 42 = Tidy up | at "Go shopping" – row 17 just moved |
With the index, React thinks row 0 is still the same row, it just got different text. So it keeps its state – the note "milk". With the ID it knows row 99 is new and just moves rows 17 and 42 along with their state.
What you see: The same task list rendered twice. On the left with key={index}, on the right with key={task.id}. Every row has its own input – its text is the row's state. The button adds a new task to the start of both lists.
Try it:
The takeaway: State belongs to the element with the given key. A position-based key gives the state to the wrong element as soon as the order changes.
Loading the interactive part…
The component already gets its props – it just doesn't print them yet.
CHALLENGE: Name badge
The Badge component gets the props `name` and `role`, but it shows question marks for now.
1. Instead of the "?" in <strong>, render the name: {name}
2. Instead of the "?" below it, render the role: {role}row, stack, card, list, btn, input or muted are the course's ready-made styles in src/styles.css (row = side by side, stack = stacked, card = bordered box, muted = grey text).Loading the interactive part…
The challenges follow the order of the sections. The first practises section 3 – you'll write a "wrapper" that knows nothing about its content. The second practises sections 4 and 5 (conditions, lists and keys).
Write a "tortilla": a panel with a heading that accepts any filling through children, plus optional slots for buttons and a footer. You'll see this pattern in every UI library.
CHALLENGE: A reusable Panel (composition with children and "slots")
The <Panel> component has its look and prop types ready. It just doesn't use the props yet.
1. In Panel, render `title` into the <strong> in the header.
2. On the right of the header, render the `actions` slot (it can be missing – then nothing renders).
3. Render `children` into the body of the panel.
4. Render the <footer> ONLY when `footer` exists.
5. In Dashboard, replace the three <div>s with the <Panel> component:
a) "Revenue" – content `revenue`, footer "Updated today"
b) "Tasks" – content `taskList`, actions `addButton`
c) "Note" – just text, no actions and no footer
The content (revenue, taskList, addButton) is already prepared in variables.row, stack, card, list, btn, input or muted are the course's ready-made styles in src/styles.css (row = side by side, stack = stacked, card = bordered box, muted = grey text).Loading the interactive part…
Render a list of products with prices, highlight the sold-out ones and add an "Only in stock" filter. The checkbox is already prepared (it uses state, which we'll cover in detail in the next lesson). The detailed brief is right below this text.
CHALLENGE: A product list
Data: the `products` array is imported at the top from src/course/fakeApi.en.ts.
Every product looks like this: { id: 1, name: 'Apple', category: 'fruit', price: 12, inStock: true }
The ProductItem component (one row of the list) and the checkbox are ready.
1. Calculate `visible`: when "Only in stock" is ticked, only products with inStock === true, otherwise all of them.
2. Render `visible` with .map() as <ProductItem> (don't forget the right `key`).
3. In ProductItem: a sold-out product gets the class "muted" and the text "(sold out)" after its name.
4. Below the list, fill in the numbers in "Showing X of Y products".
5. Bonus: group the products by category (an <h4> heading + a list for each).Loading the interactive part…
{…}.ReactNode) let a component wrap content it knows nothing about.&& – but only a boolean into &&, otherwise a 0 gets rendered.products, Product)products, Product – from the file src/course/fakeApi.en.ts. The course fake API: data and functions pretending to be a server (with a delay, sometimes even with an error). In a real app you would call fetch() here.export interface Product {id: numbername: stringcategory: 'fruit' | 'vegetables' | 'bakery' | 'dairy'price: numberinStock: boolean}export const products: Product[] = [{ id: 1, name: 'Apple', category: 'fruit', price: 12, inStock: true },{ id: 2, name: 'Banana', category: 'fruit', price: 8, inStock: true },{ id: 3, name: 'Pear', category: 'fruit', price: 15, inStock: false },{ id: 4, name: 'Carrot', category: 'vegetables', price: 6, inStock: true },{ id: 5, name: 'Tomato', category: 'vegetables', price: 9, inStock: true },{ id: 6, name: 'Cucumber', category: 'vegetables', price: 19, inStock: false },{ id: 7, name: 'Bread roll', category: 'bakery', price: 3, inStock: true },{ id: 8, name: 'Bread', category: 'bakery', price: 45, inStock: true },{ id: 9, name: 'Milk', category: 'dairy', price: 22, inStock: true },{ id: 10, name: 'Cheddar', category: 'dairy', price: 39, inStock: true },{ id: 11, name: 'Yogurt', category: 'dairy', price: 14, inStock: false },{ id: 12, name: 'Orange', category: 'fruit', price: 11, inStock: true },]
row, stack, card, list, btn, input or muted are the course's ready-made styles in src/styles.css (row = side by side, stack = stacked, card = bordered box, muted = grey text).