React 01 · Basics
A quick review of the basics
JSX, components, props, children, conditions, lists and why key matters.
Loading lesson…
React 01 · 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. You know the type after the colon ({ name: string }) from lesson TypeScript 01: the order must contain the name as text. Inside curly braces { } you can then put a value from JavaScript into JSX:
function Welcome({ name }: { name: string }) {return (<section className="welcome"><h1>Hello, {name}!</h1></section>)}
The recipe is ready. The order goes somewhere else – into the JSX of another component that wants to show the greeting. Here it's the main component App, which uses the recipe twice:
function App() {return (<div>{/* One recipe, two orders → two dishes */}<Welcome name="Anna" /><Welcome name="Peter" /></div>)}
<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 – the definition, which you write once (say in the file Welcome.tsx):
// ① THE DEFINITION of the component (the recipe)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>)}
The definition on its own shows nothing. The component has to be used – in another component's JSX it's written as a tag. Here the main component App uses it:
// ② USING the component – in another component's JSX it's written as a tagfunction App() {return (<main><Welcome /></main>)}
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.
Code with props always has two parts in two different places: ① the recipe – the component's definition, which you write once (usually in its own file), and ② the order – the place where you use the component in another component's JSX. The recipe first:
// ① THE RECIPE – the component's definition, say in the file ProfileCard.tsx// Which props the component accepts (the shape of the order):interface ProfileCardProps {name: stringrole?: string // question mark = optionalavatar?: string}// ↓ React puts here the attributes you write on the tag in ②function ProfileCard({ name, role = 'Developer', avatar = '🙂' }: ProfileCardProps) {return (<div className="card"><span>{avatar}</span><strong>{name}</strong><div>{role}</div></div>)}
And here's the order. The App component wants to show two cards, so it writes the <ProfileCard … /> tag into its JSX twice – each time with different attributes:
// ② THE ORDER – using the component in another component's JSXfunction App() {return (<div className="row">{/* Two different orders, one recipe: */}<ProfileCard name="Anna Smith" role="Frontend" avatar="👩💻" /><ProfileCard name="Peter Brown" /></div>)}
What travels from the order ② into the recipe ①:
| Attribute on the tag ② | Variable in ProfileCard ① | For Peter (no role or avatar) |
|---|---|---|
name="Anna Smith" | name is 'Anna Smith' | name is 'Peter Brown' |
role="Frontend" | role is 'Frontend' | role is 'Developer' (the default value) |
avatar="👩💻" | avatar is '👩💻' | avatar is '🙂' (the default value) |
What's going on here:
App React collects the attributes into the object { name: 'Anna Smith', role: 'Frontend', avatar: '👩💻' } and calls the ProfileCard function 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.
// ① THE RECIPE – a card with a title; it gets its filling in childrenfunction Card({ title, children }: { title: string; children: React.ReactNode }) {return (<div className="card"><h3>{title}</h3><div className="card-body">{children}</div></div>)}
And using it in another component. Whatever is between <Card …> and </Card>, Card gets as children:
// ② THE ORDERS – the same card, a different filling each timefunction App() {return (<div className="row"><Card title="News"><p>React 19 is out!</p> {/* ← the first card's children */}</Card><Card title="Team"><ProfileCard name="Anna" /> {/* ← the second card's children: */}<ProfileCard name="Peter" /> {/* both cards from section 2 */}</Card></div>)}
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), it gets more props of type ReactNode. Such places are called slots. Take a panel with buttons in the header and a footer – its recipe first:
// ① THE RECIPE – a panel with three slots: actions, footer and childreninterface PanelProps {title: stringactions?: React.ReactNode // a slot in the header (optional)footer?: React.ReactNode // a slot in the footer (optional)children: React.ReactNode // the main content}function Panel({ title, actions, footer, children }: PanelProps) {return (<section className="card"><header><strong>{title}</strong>{actions}</header><div>{children}</div>{footer !== undefined && <footer>{footer}</footer>}</section>)}
And the order – the arrows show what goes where:
// ② THE ORDERfunction App() {return (<Paneltitle="Tasks"actions={<button>+ Add</button>} // → {actions} in the headerfooter={<small>3 tasks</small>} // → {footer} in the footer><ul> {/* → {children} in the middle */}<li>Go shopping</li><li>Clean up</li><li>Pay the rent</li></ul></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 !== undefined && …}. 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.
// The data – an array of tasksconst tasks = [{ id: 17, label: 'Go shopping' },{ id: 42, label: 'Tidy up' },]// ① THE RECIPE – one row of the listfunction TaskRow({ task }: { task: { id: number; label: string } }) {return <li>{task.label}</li>}// ② USING IT – the list: map turns every task into one <TaskRow>function TaskList() {return (<ul>{tasks.map((task) => (<TaskRow key={task.id} task={task} />))}</ul>)}
What in the example is fixed and what did you name yourself? Only two words are fixed – key and map. You can name everything else your own way:
| Word | Who decides it | What it means |
|---|---|---|
key | React – it must be called exactly this | the cloakroom ticket; React takes it for itself and doesn't pass it to the component |
map | JavaScript – an array method | turns every item of the array into one element |
tasks | you | the array with the data – todos or items would do just as well |
task | you | the one item map is on right now (the callback parameter) |
id, label | you (or the database) | the item's properties; id makes a good key, because it's different for every item |
TaskRow | you | the component for one row – it just has to start with a capital letter |
task={task} | you | an ordinary prop: its name on the left, the variable you send into it on the right |
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 JavaScript 04.)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.
Task is in the comments in the code below – at the top of the file and at the places marked TODO.
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. What should show at the end is in the comments in the code.
Task is in the comments in the code below – at the top of the file and at the places marked TODO.
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). What should show is in the comments in the code.
Task is in the comments in the code below – at the top of the file and at the places marked TODO.
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).