The first component
In short: A React component is a function that returns JSX: it describes how the screen looks for the data, and React changes only the difference on the page.
Problem
The field loses focus
The module "JavaScript in the page" ended with a cart drawn from the data by one render(). Here every change of the quantity field calls render(). The buyer wants 15 bottles of milk: they click into the field, delete 2 and type 15. Try it.
Console
After the delete the field is empty and no longer focused: 1 and 5 go nowhere. render() replaced the field with a new one on the very first key. The new field looks the same, but it is a different element, and the focus was on the old one. Touching separate places by hand forgets places. Drawing everything again loses the focus. There is a third way.
The idea
The interface as a function of the data
render() had a good idea: the screen is built only from the data. With the same data the screen is the same, whatever was on it before. The mistake was in the last step: render() removes and creates the elements itself.
So the work is split in two:
- You write a function that says how the screen looks for these data. It does not touch the page; it returns a description: here is a
<main>, in it an<h1>with the text "The cart", then a field with the value 2. - The library holds the real DOM. The first time it builds it from the description. On every change of the data it takes the new description, compares it with the old one and changes only the difference on the page.
Such a function is called a component. In short: the screen is a function of the data, and the component is that function. React and Vue do this in different ways. The idea is the same, and choosing between them is a matter of the team, not of a "better" library.
See it
The first component
In React a component is a function that returns JSX: notation that looks like HTML but lives in JavaScript.
const product = { name: 'Fresh milk', price: 1.6, stock: 35 };
export default function App() { return ( <main> <h1>{product.name}</h1> <p>Price: {money(product.price)}</p> </main> );}function App()is an ordinary function. The name starts with a capital letter: that is how React tells a component from an HTML tag.return ( … )returns the description. The parentheses are only there so JSX can start on a new line.{product.name}is a JavaScript expression in curly braces. Its result becomes text on the page.export defaultlets another file import the component. The sandbox putsAppon the page by itself. In a real project one line inmain.jsxdoes it:createRoot(...).render(<App />).
Console
Change the price or the stock in the object product and press "Run". You do not touch the description of the screen: the screen follows the data.
The idea
The rules of JSX
JSX looks like HTML, but it is not HTML: before it runs, every tag becomes a function call in JavaScript. That is why it has rules of its own.
- One root.
returnreturns one element. If several must sit side by side, wrap them in a fragment<>…</>: it does not create an element on the page. classNameinstead ofclass,htmlForinstead offor.classandforare JavaScript words. Withclassthe page still works, but React logs an error in the console: "Invalid DOM propertyclass. Did you meanclassName?".{expression}in text and in attributes:<img src={product.image} alt={product.name} />. No quotes around the braces:alt="{product.name}"is the text{product.name}, letter by letter.- Every tag is closed, including those without content:
<input />,<img />,<br />. - Events are camelCase and get a function:
onClick={add}. NotonClick={add()}: that callsaddright away, while the component is being drawn, and givesonClickits result.
Try it
Find the bug
A click on "Add" writes nothing to the console.
Pick the line with the bug.
onclick="add()" is HTML notation. React takes events only as onClick with a function: onClick={add}. It skips this attribute and logs an error in the console: "Invalid event handler property onclick. Did you mean onClick?".Try it
Fill in
The screen reader should read the product name as the description of the image.
<img src={product.image} alt= />alt={product.name}: the expression is in curly braces, without quotes.The idea
Components from components
The shop page has parts: the top with the menu, the product card, the cart. Each part becomes a component in its own file, and a bigger component uses them as tags. Then App is not one long function but a few lines that say what is on the page.
export default function ShopHeader() { return ( <header> <a href="/">The shop</a> <nav>…</nav> </header> );}import ShopHeader from './ShopHeader';import ProductCard from './ProductCard';
export default function App() { return ( <> <ShopHeader /> <main> <ProductCard /> </main> </> );}The component name starts with a capital letter, and that is not a matter of taste. For React <ShopHeader /> is a component, and <shopHeader /> is an HTML tag <shopheader> that the browser does not know. Then the console has an error, and the top part is missing. The fragment <>…</> is there because ShopHeader and <main> sit side by side.
Try it
Put the page together
ShopHeader and ProductCard are ready in their files. Put App together: ShopHeader at the top, and the product card in main, under the heading "Dairy".
Console
Solution
App.jsximport ShopHeader from './ShopHeader';
import ProductCard from './ProductCard';
export default function App() {
return (
<>
<ShopHeader />
<main>
<h1>Dairy</h1>
<ProductCard />
</main>
</>
);
}
ShopHeader.jsxexport default function ShopHeader() {
return (
<header>
<a href="/">The shop</a>
<nav>
<a href="/bread">Bread</a>
<a href="/milk">Milk</a>
<a href="/cart">Cart</a>
</nav>
</header>
);
}
ProductCard.jsxconst product = { name: 'Fresh milk', price: 1.6, stock: 35 };
const money = (x) => '€' + x.toFixed(2);
export default function ProductCard() {
return (
<article>
<h2>{product.name}</h2>
<p>Price: {money(product.price)}</p>
</article>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; background: #f3efe6; padding: 8px 16px; }
nav a { margin-left: 12px; }
main { padding: 0 16px; }
h1 { font-size: 22px; }
label { display: block; margin: 8px 0; }
input { width: 4em; font: inherit; margin-left: 8px; }
article { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 12px 12px; max-width: 320px; }
button { font: inherit; }
App imports the two components with import and uses them as tags: <ShopHeader /> before <main>, <ProductCard /> in <main>. They sit side by side, so they are in a fragment <>…</>. The check looks for the top part by role: a <header> inside <main> is no longer the top of the page, and a lowercase <shopHeader /> has no <header> at all.
The idea
Only the difference
Here is the cart from the problem as a component.
useState('2') is the memory of the component: a value React keeps between two drawings, and a function (setMilk) that changes it and tells React to draw the component again. It is the topic of the module "State". Here it is only so that something can change.
Console
Do the same as in the problem: click into the milk field, delete 2 and type 15. The field keeps the focus and what you typed, and the total becomes €25.20. The component describes the whole screen on every key, like render(). The difference is that the page is not built again. The library sees that in the new description the field is in the same place with the same tag, and changes only its value and the text of the total.
See it
See the comparison
import { useState } from 'react';const money = (x) => '€' + x.toFixed(2);
export default function App() { const [milk, setMilk] = useState('2'); const [bread, setBread] = useState('1'); const total = Number(milk) * 1.6 + Number(bread) * 1.2;
return ( <main> <h1>The cart</h1> <label> Fresh milk <input type="number" value={milk} onChange={(e) => setMilk(e.target.value)} /> </label> <label> White bread <input type="number" value={bread} onChange={(e) => setBread(e.target.value)} /> </label> <p>Total: {money(total)}</p> </main> );}Step by step, in words
- The buyer has deleted 2 and typed 1: the data say milk = "1". In the middle is the last description the component returned; on the right is the DOM, with the focus in the milk field.
- The buyer types 5. onChange calls setMilk("15"): the data change, and React notes that App has to be drawn again.
- React calls the function App again. It does not touch the page: it returns a new description, a tree of plain objects. Building it is cheap.
- The library compares the new description with the previous one. Same tags in the same order; two differences: the value of the field and the text of the total.
- Only the two differences reach the DOM. The <input> is the same element as before, so the focus and the cursor stay in it.
- You describe the whole screen for the given data, as with render(). The library finds what changed and touches only that. Both libraries do this, each in its own way.
The description is cheap: it is a tree of plain objects, and the component makes it again every time. The DOM is the expensive part, so the library touches only the differences there. You write as with render(): the whole screen from the data. The price of render() (lost focus, thousands of new elements) is not paid.
More precisely, if you are curious
Virtual DOM. The description is often called the virtual DOM, and comparing it with the old one is called reconciliation (React) or patching (Vue). A node of the description is an object like { type: 'h1', props: { children: 'The cart' } }.
The two ways. JSX compiles to calls like jsx('h1', { children: product.name }). When the memory of a component changes, React calls its function again, and the functions of the components inside it, then compares. In both cases there is a new description at the end, a comparison and a few changes in the DOM.
Text is text. {product.name} puts in text, like textContent. A name like <img src=x onerror=alert(1)> shows as characters and does not become a tag. HTML from a string goes in only with dangerouslySetInnerHTML, and the name tells you to be careful.
Class components. In older code a component is a class: class Cart extends React.Component { render() { … } }. It still works, but new code uses functions and hooks (useState is a hook).
StrictMode. A new Vite project wraps App in <StrictMode>. In development mode it calls every component twice, to bring out code that touches something outside itself. That is why a console.log in the body of a component can appear twice. The sandbox draws App without StrictMode.
Exercise
The product card
ProductCard holds the object product (White cheese). Make the card: the name as a level-two heading, "Price: €7.90" (with money), "In stock: 12 pcs" and an "Add" button whose name for the screen reader is "Add White cheese". A click on the button writes "Added: White cheese" to the console (the function add is ready). Then put App together: ShopHeader at the top, and the card in main, under the heading "Dairy".
Console
Solution
ProductCard.jsxconst product = { name: 'White cheese', price: 7.9, stock: 12 };
const money = (x) => '€' + x.toFixed(2);
export default function ProductCard() {
function add() {
console.log(`Added: ${product.name}`);
}
return (
<article>
<h2>{product.name}</h2>
<p>Price: {money(product.price)}</p>
<p>In stock: {product.stock} pcs</p>
<button type="button" aria-label={`Add ${product.name}`} onClick={add}>
Add
</button>
</article>
);
}
App.jsximport ShopHeader from './ShopHeader';
import ProductCard from './ProductCard';
export default function App() {
return (
<>
<ShopHeader />
<main>
<h1>Dairy</h1>
<ProductCard />
</main>
</>
);
}
ShopHeader.jsxexport default function ShopHeader() {
return (
<header>
<a href="/">The shop</a>
<nav>
<a href="/bread">Bread</a>
<a href="/milk">Milk</a>
<a href="/cart">Cart</a>
</nav>
</header>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; background: #f3efe6; padding: 8px 16px; }
nav a { margin-left: 12px; }
main { padding: 0 16px; }
h1 { font-size: 22px; }
label { display: block; margin: 8px 0; }
input { width: 4em; font: inherit; margin-left: 8px; }
article { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 12px 12px; max-width: 320px; }
button { font: inherit; }
Everything from the data is in {…}: {product.name}, {money(product.price)}, {product.stock}. The name for the screen reader is an expression: aria-label={`Add ${product.name}`}. The button gets the function: onClick={add}. With onClick={add()} "Added: White cheese" shows up in the console already on load, without a click, and the check looks for exactly that.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Accessibility · The DOM: the page from JavaScript · Events · A list from data
- Continues in: Props: data from above · Lists and keys · Conditions: empty, error, loading · State
- Tasks on this topic: Components