React: interfaces built from components
Lesson 5 of 6
5React: interfaces built from components
The big pictureIn the DOM lesson you wrote render() by hand and redrew the whole list on every change. In an app with a hundred screens that gets hard. React takes the same idea and automates it: you describe the interface for each state, and it works out the smallest change to the page and applies it. That is why it became the most popular interface library in the world.
What the picture shows: A component tree: data flows down from App through props, events flow up through functions that change state, and React redraws only what changed.
Map of this part
The concepts in order, click any one to jump to it:
- Why React? From a hand-written render to UI = f(state)
- Create your first project with Vite
- Components and JSX
- props: a component's inputs, and composition
- State: useState
- Events and controlled forms
- Lists, keys and conditional rendering
- Lifting state up: who owns the data?
- useEffect: fetching data and syncing with the outside
Two ways into the same idea
React is like LEGO: you build a "button" once and use it everywhere, build a "product card" from a button, an image and text, and build a "page" from cards. Each piece is a component. Instead of telling the page "remove this, add that, update the counter", you tell React "this is the task list now" and it arranges the page to match, like an assistant comparing two pictures and changing only the difference.
React is a declarative UI library: a component is a near-pure function from props and state to React elements (light objects describing the DOM). JSX is syntax sugar the compiler (Babel, or esbuild in Vite) turns into jsx()/createElement calls. When state changes, React calls the component again, compares the new tree with the old (reconciliation), then applies the difference to the DOM. useState keeps state across renders and schedules a re-render, and updates must be immutable because React compares by reference (Object.is). A key gives list items identity during reconciliation. useEffect synchronises with external systems through a dependency array and a cleanup function, and in StrictMode it runs twice in development to expose unclean effects. The rules: hooks are called at the top of the component only, never inside a condition or loop.
Before You Start
🖱️JavaScript and the DOM
You built the to-do list with a hand-written render.
⏳Asynchronous JavaScript
You know fetch, async/await and loading states.
📦Node.js and npm
Installed so you can run npm create vite@latest, from the Express lesson.
The Core Idea
1Why React? From a hand-written render to UI = f(state)
Remember the to-do list in the DOM lesson: every event changes the state, then render() rebuilds the whole list. That is correct but costly and long: in a big app you write thousands of createElement lines. React says: describe the interface as a function that takes state and returns the look, and I will handle the rest. The next example imitates the idea in plain JavaScript, so you see React is not magic:
// a component is just a function: state in, description out
const Item = (todo) => `<li${todo.done ? ' class="done"' : ""}>${todo.text}</li>`;
const App = (state) => `<ul>${state.todos.map(Item).join("")}</ul><p>${state.todos.filter((t) => !t.done).length} left</p>`;
let state = { todos: [{ text: "buy beans", done: false }] };
let screen = App(state);
console.log("first render:\n ", screen);
// an event: we never touch the screen, only the state
state = { todos: [...state.todos, { text: "grind coffee", done: true }] };
const next = App(state);
// React compares the old and new description and applies only the difference
console.log("changed?", next !== screen);
console.log("new render:\n ", next);That is the whole idea of React: interface = function(state). You change only the state, and React calls the component functions again, compares the result with the last one, and edits only the changed elements in the real DOM.
2Create your first project with Vite
Vite is a tool that creates a ready React project with one command and runs it on a dev server that refreshes the browser the moment you save. In the terminal:
npm create vite@latest cafe -- --template react cd cafe npm install npm run dev # open http://localhost:5173
The project layout: one HTML page with an empty element, and React fills it with components.
cafe/
index.html <- the single HTML page, with <div id="root">
package.json <- dependencies and scripts (dev, build)
src/
main.jsx <- mounts <App /> into #root
App.jsx <- your first component: edit it and save
App.css3Components and JSX
A component is a function whose name starts with a capital letter and that returns JSX: HTML-like markup written inside JavaScript. You use it like a tag: <Price />. How JSX differs from HTML: className instead of class, htmlFor instead of for, any JavaScript value inside curly braces {}, every tag is closed even empty ones (<img />), and a component returns a single parent element (or <>...</> if you want no wrapper).
function Price({ amount, currency = "SAR" }) {
return <span className="price">{amount} {currency}</span>;
}
function ProductCard({ name, amount, image }) {
return (
<article className="card">
<img src={image} alt={name} />
<h3>{name}</h3>
<Price amount={amount} />
</article>
);
}
export default function App() {
return (
<main>
<ProductCard name="Latte" amount={14} image="/latte.jpg" />
<ProductCard name="Mocha" amount={16} image="/mocha.jpg" />
</main>
);
}JSX is not magic: the compiler turns every tag into a call createElement(type, props, children). The next playground loads the real React from the internet and uses createElement directly with no tools, so you see what happens under JSX:
Real React running here
Press + and − on each counter: every copy of the component has its own state. Add a third counter for "V60" with one line. (Needs an internet connection to load React.)
4props: a component's inputs, and composition
props are what a parent passes to a component, like function arguments: <Price amount={14} /> arrives as { amount: 14 }. They are read-only: a component never changes the props it received. The special children prop holds whatever is between the opening and closing tags, so you build "wrapper" components that accept any content.
function Card({ title, children }) {
return (
<section className="card">
<h2>{title}</h2>
{children}
</section>
);
}
<Card title="Today's offer">
<p>Two lattes for 20 SAR.</p>
<button>Order</button>
</Card>5State: useState
const [count, setCount] = useState(0) gives you a value and a function to change it. When you call setCount(5), React remembers the new value and redraws the component with it. State is local: every copy of the component has its own. When the new value depends on the old one, pass a function: setCount((c) => c + 1).
import { useState } from "react";
function Counter() {
const [cups, setCups] = useState(1);
return (
<div>
<button onClick={() => setCups((c) => Math.max(1, c - 1))}>-</button>
<output>{cups}</output>
<button onClick={() => setCups((c) => c + 1)}>+</button>
<p>Total: {cups * 14} SAR</p>
</div>
);
}Why not change state directly? React detects a change by comparing the reference: is it the same array, or a new one? Run this to see the difference:
const todos = ["buy beans"];
// wrong: push changes the same array, so the reference is identical
const mutated = todos;
mutated.push("grind coffee");
console.log("push -> same reference?", mutated === todos, "(React sees no change)");
// right: spread builds a new array, so React knows to redraw
const fresh = [...todos, "brew"];
console.log("spread -> same reference?", fresh === todos, "(React redraws)");todos.push(newTodo); setTodos(todos). The array is the same reference, so React sees no change and does not redraw. Write setTodos([...todos, newTodo]), use map to edit and filter to delete.6Events and controlled forms
Events in JSX are capitalised props: onClick, onChange, onSubmit, and you give them a function, not a function call (onClick={add}, not onClick={add()}). A controlled field takes its value from state (value={text}) and updates it on every keystroke (onChange), so state and screen always match and you can validate or format while typing.
function SignUp() {
const [email, setEmail] = useState("");
const valid = email.includes("@");
function handleSubmit(e) {
e.preventDefault(); // same as the DOM lesson
if (valid) console.log("send", email);
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input id="email" value={email} onChange={(e) => setEmail(e.target.value)} />
{!valid && email && <p className="error">Email needs an @</p>}
<button disabled={!valid}>Sign up</button>
</form>
);
}onClick={remove(todo.id)} runs the delete immediately during render, not on click. The right way wraps it in a function: onClick={() => remove(todo.id)}.7Lists, keys and conditional rendering
You render a list with map returning JSX for each item, and each item needs a stable, unique key from the data (an id, not the index). The key lets React know which item was removed or moved, so it keeps the right state. For conditional rendering: {condition && <X />} shows X when the condition holds, and {condition ? <A /> : <B />} chooses between them.
function Menu({ drinks }) {
if (drinks.length === 0) return <p>The menu is empty today.</p>;
return (
<ul>
{drinks.map((d) => (
<li key={d.id}>
{d.name} · {d.price} SAR
{d.soldOut ? <em> (sold out)</em> : <button>Add</button>}
</li>
))}
</ul>
);
}8Lifting state up: who owns the data?
If two components need the same data (the form adds a task, the list shows it), put the state in their nearest common parent, pass it down as props, and pass functions that change it along with it. This is called "lifting state up". The rule: data flows down, events flow up.
App <- owns: todos, and the functions add / toggle / remove
/ \
TodoForm TodoList props down: todos, onToggle, onDelete
onAdd(text) ↑ \ events up: onAdd("grind coffee")
TodoItem × n9useEffect: fetching data and syncing with the outside
Whatever is not drawing (fetching from the server, saving to localStorage, a timer, a subscription) goes in useEffect. It runs after rendering, and the second argument says when it re-runs: [] once when it appears, and [todos] whenever the tasks change. The function it returns (optional) cleans up before the next run or when it disappears. With fetch you need three pieces of state, as in the asynchronous lesson: the data, loading, and the error.
function MenuPage() {
const [drinks, setDrinks] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch("/api/menu", { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(`server said ${res.status}`);
return res.json();
})
.then(setDrinks)
.catch((err) => { if (err.name !== "AbortError") setError(err.message); })
.finally(() => setLoading(false));
return () => controller.abort(); // cleanup: cancel if we leave the page
}, []); // [] = run once
if (loading) return <p>Loading the menu...</p>;
if (error) return <p role="alert">Could not load: {error}</p>;
return <Menu drinks={drinks} />;
}Worked example
The to-do list as components: state lives in App, the form lifts text up through an onAdd function, the list renders items with keys, and useEffect saves to localStorage. Compare it line by line with the DOM lesson's version: exactly the same logic, but with no createElement and no replaceChildren.
import { useEffect, useState } from "react";
function TodoForm({ onAdd }) {
const [text, setText] = useState("");
return (
<form onSubmit={(e) => { e.preventDefault(); if (text.trim()) onAdd(text.trim()); setText(""); }}>
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="New task" aria-label="New task" />
<button>Add</button>
</form>
);
}
function TodoItem({ todo, onToggle, onDelete }) {
return (
<li className={todo.done ? "done" : ""}>
<label>
<input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} />
{todo.text}
</label>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
}
export default function App() {
const [todos, setTodos] = useState(() => JSON.parse(localStorage.getItem("todos") ?? "[]"));
const [filter, setFilter] = useState("all");
useEffect(() => localStorage.setItem("todos", JSON.stringify(todos)), [todos]);
const add = (text) => setTodos((t) => [...t, { id: crypto.randomUUID(), text, done: false }]);
const toggle = (id) => setTodos((t) => t.map((x) => (x.id === id ? { ...x, done: !x.done } : x)));
const remove = (id) => setTodos((t) => t.filter((x) => x.id !== id));
const shown = todos.filter((t) => filter === "all" || (filter === "done" ? t.done : !t.done));
return (
<main>
<TodoForm onAdd={add} />
<div role="group" aria-label="Filter">
{["all", "done", "left"].map((f) => (
<button key={f} aria-pressed={filter === f} onClick={() => setFilter(f)}>{f}</button>
))}
</div>
<ul>
{shown.map((t) => <TodoItem key={t.id} todo={t} onToggle={toggle} onDelete={remove} />)}
</ul>
<p>{todos.filter((t) => !t.done).length} left</p>
</main>
);
}Myth vs Reality
| Myth | Reality |
|---|---|
| React is a full framework that handles everything. | It is a UI library. Routing, data fetching and the server come from frameworks on top, like Next.js. |
| The index is fine as a key. | It breaks when you delete or reorder, moving one item's state to another. Use an id from the data. |
| Learn React directly and skip JavaScript. | React is JavaScript: map, filter, spread and arrow functions are on every line. Skipping them means stumbling. |
Where you meet it at work
🏢Major products
Facebook, Instagram, Netflix and many local apps.
📱React Native
The same ideas build real Android and iOS apps.
💼The job market
The most requested front-end skill in job ads.
Test your understanding
You added an item with todos.push(x) then setTodos(todos) and the page did not update. Why?
React compares references. Create a new array with spread.
Where do you put fetching data from the server in a React component?
Fetching is a side effect outside rendering; it belongs in useEffect.
How does a child component send data to its parent?
Data flows down as props; events flow up through functions like onAdd.
What is wrong with <button onClick={save()}>?
Pass the function itself or wrap it: onClick={() => save()}.
What is key for in a list?
With the key React matches items between two renders and keeps their state.
Quick review cards
Check Your Understanding
What does UI = f(state) mean?
The interface is a deterministic result of state. You change only state; React recomputes the interface.
Why not change props inside the child?
Because they belong to the parent and come from its state. The child asks for change through a function, and the parent changes its state.
What happens if you omit the dependency array in useEffect?
The effect runs after every render and may loop forever if it changes state.
What is a controlled field?
A field whose value comes from state and updates it on every change, so state, not the DOM, is the source of truth.
Resources for this lesson
Grouped by type. Start with the official docs for precision, or an article or video for the explanation.
Official docs4
Done with this lesson?
Sign in to save your progress.