A reading shelf contains repeated pieces: a title, a search field, several book entries, and a description that appears when an entry is selected. With direct DOM programming, we write instructions for finding and changing those elements. With React, we describe the interface for the current data, then update that data when an event occurs. React calculates the resulting interface and commits the necessary browser DOM changes.
This change of organisation matters when several elements depend on the same choice. The selected book's button label, pressed state and description should all agree. A single state value can determine those outputs together.
Components describe a view of data
Definition
Component, props and state
A component is a reusable unit that describes part of an interface. Props are values supplied by its parent. State is information React retains for a component between renders. A render computes an interface description from the props and state available to that render.
A function component returns React elements. Its name begins with a capital letter so that JSX distinguishes it from built-in elements such as p and button.
function ShelfHeading({ title, total }) {
return (
<header>
<h1>{title}</h1>
<p>{total} books available</p>
</header>
);
}
function ShelfIntro() {
return <ShelfHeading title="Reading shelf" total={3} />;
}
ShelfIntro supplies two props. ShelfHeading reads them through parameter destructuring. The string is written in quotes; the number is passed as a JavaScript expression inside braces. Rendering two ShelfHeading instances with different props reuses the same component definition while producing different content.
A component treats its props as read-only inputs. An object passed as a prop is still a JavaScript object, so changing one of its fields would mutate shared data. The component should request a change through a callback supplied by the owner of that data. Keeping the ownership clear makes an interface easier to trace.
React is a library for describing interfaces; a larger application may add routing, data access and build tools around it. Neither component reuse nor a virtual tree guarantees that every implementation is fast. The useful starting point is a clear relationship between data and the displayed result.
JSX is JavaScript syntax for element descriptions
JSX resembles HTML, but it is transformed into JavaScript before a normal browser executes it. The resulting value describes React elements; it is not an HTML source string. A project build can perform this transformation and resolve imports from installed React packages.
The examples in this unit are JSX modules for a React project with that transformation configured. A client-rendered entry point can mount an exported ReadingShelf component into an existing empty div whose ID is app:
import { createRoot } from "react-dom/client";
import ReadingShelf from "./ReadingShelf.jsx";
const container = document.getElementById("app");
const root = createRoot(container);
root.render(<ReadingShelf />);
The container must exist before the entry point runs. Call createRoot once for this root and let its components manage the content inside it. Existing React-generated server HTML uses hydration rather than this empty-container mounting procedure. The older ReactDOM.render entry API belongs to earlier React versions and is removed in React 19.
Several JSX rules are visible in even a small component:
| Rule | Example | Reason |
|---|---|---|
| Group adjacent JSX elements | <>...</> | A return expression produces one value. A fragment groups children without adding a DOM wrapper. |
| Close every element | <input /> | JSX requires closing syntax even for HTML void elements. |
| Use JSX property names | className, htmlFor, onClick | Properties follow the React DOM interface. aria-* and data-* keep their hyphens. |
| Put JavaScript expressions in braces | {book.title} | The expression is evaluated as JavaScript. Statements such as if belong outside that expression or in a function. |
| Supply a style object | style={{ maxWidth: "36rem" }} | The outer braces enter JavaScript; the inner braces construct an object. Style property names use camelCase. |
Plain strings inserted with braces are displayed as text. For example, a book title containing <draft> is shown as those characters. An object itself is not a suitable text child: render book.title rather than {book}.
A reading shelf with one owner for selection
Worked example
Compose a complete interface from smaller components
Place this module in ReadingShelf.jsx in the React project. All data and interactions remain in the browser's current component tree.
import { useState } from "react";
const books = [
{ id: "markup", title: "Meaningful markup", note: "Describe content relationships." },
{ id: "events", title: "Event traces", note: "Follow one action through its state changes." },
{ id: "layout", title: "Flexible layouts", note: "Adapt presentation to available space." },
];
function BookEntry({ book, selected, onChoose }) {
return (
<li>
<h2>{book.title}</h2>
<button
type="button"
aria-pressed={selected}
onClick={() => onChoose(book.id)}
>
{selected ? "Hide discussion note" : "Show discussion note"}
</button>
{selected && <p>{book.note}</p>}
</li>
);
}
export default function ReadingShelf() {
const [query, setQuery] = useState("");
const [selectedId, setSelectedId] = useState(null);
const search = query.trim().toLowerCase();
const visibleBooks = books.filter(book =>
book.title.toLowerCase().includes(search)
);
function chooseBook(id) {
setSelectedId(previous => previous === id ? null : id);
}
return (
<main style={{ maxWidth: "36rem" }}>
<h1>Reading shelf</h1>
<label htmlFor="reading-query">Search title</label>
<input
id="reading-query"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<p>{visibleBooks.length} matching books</p>
{visibleBooks.length === 0 ? (
<p>No titles match this search.</p>
) : (
<ul>
{visibleBooks.map(book => (
<BookEntry
key={book.id}
book={book}
selected={selectedId === book.id}
onChoose={chooseBook}
/>
))}
</ul>
)}
</main>
);
}
Start with the initial state and follow a search and selection:
| Action | query | selectedId | Visible result |
|---|---|---|---|
| Initial render | "" | null | All three titles appear; no discussion note is shown. |
Enter event | "event" | null | Only Event traces appears; the matching count is 1. |
| Select that entry | "event" | "events" | Its note, “Follow one action through its state changes.”, appears. |
Change the search to layout | "layout" | "events" | Only Flexible layouts appears, without a note; the selected book is filtered out. |
| Clear the search | "" | "events" | All three titles return, and the Event traces note is visible again. |
The shelf owns query and selectedId. A BookEntry receives its book, a Boolean selection value and a callback. Clicking the button calls the callback with the book ID. The shelf updates its state, and a later render computes new props for every visible entry. A search changes query without clearing selectedId, which explains the final two rows.
The description, label and aria-pressed value are derived from the same selected prop. They therefore express one choice consistently. We do not keep a separate state value for each of those outputs. Similarly, visibleBooks and its length are calculated from books and query; keeping another independently updated “matching count” would introduce an unnecessary consistency problem.
The data flows from the shelf to the entries through props. An event reaches the owner through a function passed as a prop. This does not let a child modify the parent's state directly: the parent defines what the callback does.
Checkpoint
Trace one choice through the component tree
Initially selectedId is null. Click the entry whose ID is events, then click the entry whose ID is layout, then click layout again. Which description is visible after each click?
Solution · Trace the parent state and derived props
The state becomes events, so only that entry receives selected=true. It next becomes layout, so the events description disappears and the layout description appears. The final updater sees that the previous ID already equals layout and returns null; both descriptions are hidden. The one selected ID determines the state of all visible entries.
State updates schedule another render
useState(initialValue) returns a state value for the current render and a setter function. Calling the setter requests an update. It does not rewrite the local variable already captured by the running handler. When an update depends on the preceding state, an updater function expresses that dependency explicitly.
Worked example
Follow two pending state updates
Assume a component displays pages, initialises it with useState(4), and calls this handler once when its button is clicked:
function recordTwoPages() {
setPages(previous => previous + 1);
setPages(previous => previous + 1);
}
Starting at pages = 4, the first updater produces 5 and the second receives that pending result and produces 6. In contrast, two calls to setPages(pages + 1) from the same render both request the replacement value 5. Reading the captured pages immediately after either setter still reads 4 in that handler.
The next committed display is therefore 6 for the two updater functions, or 5 for the two replacement requests. The number of setter calls alone does not determine the increment; the queued operations do.
| Stage | Available information | Consequence |
|---|---|---|
| Event handler | State snapshot from its render | It can compute values and queue updates. |
| Render | Updated state and current props | React calls components to calculate the next interface. |
| Commit | Calculated changes | React updates the browser DOM to reflect the result. |
Rendering should calculate the interface without modifying unrelated objects or creating timers. React can call rendering logic again, so an observable action placed there can occur more often than the author expects. A button's user action belongs in its handler; synchronizing an external browser facility belongs in appropriate lifecycle work.
Common mistake
Changing a variable or mutating an object does not request the intended state update
A local variable is recreated when a function component runs. Assigning to it does not ask React to render. Likewise, mutating an object already stored in state and passing back the same object reference can prevent the expected update. Construct the next object or array and use the setter, for example setBook(previous => ({ ...previous, read: true })).
Events, conditions and controlled fields
onClick={chooseBook} passes a function. onClick={chooseBook(id)} calls that function during rendering and uses its return value as the event prop. To supply an argument when the click happens, pass an arrow function such as onClick={() => chooseBook(id)}.
The conditional expression selected ? a : b chooses one result. The expression selected && <p>...</p> produces the paragraph when selected is true and a non-displayed Boolean when false. For numeric counts, write a Boolean condition such as count > 0 && ...: count && ... can display 0 because zero is a renderable number.
The search input is controlled. Its displayed value comes from query; its change handler copies the newly typed text into that state.
Worked example
Trace a search edit from input to rendered list
Start with the shelf's empty query: all three books match, the input is empty, and the matching count is 3. Suppose an edit changes the input text to event.
- The change handler reads
event.target.value, whose value is now the string"event". setQueryrequests that string as the next state.- The next render computes
search = "event"and filters the titles. OnlyEvent tracescontains that search text. - The commit displays one book and the count
1, and supplies"event"back to the input as itsvalue.
The field and list now agree because both derive from query. If the handler did not update that state, the controlled field would continue receiving its previous empty value and the list would still show all three books.
Initialise a controlled text field with a string, and keep its value a string throughout its lifetime. Controlled checkboxes use checked and read event.target.checked, rather than reading their text value.
A key identifies an item across renders
map turns the visible data into entry elements. React uses each key to match sibling items between renders. An ID already belonging to a book identifies that book even when another book is removed or the order changes. The key is special reconciliation information; a child that needs the ID must also receive it in an ordinary prop such as book.
Using an array index as the key identifies a position. If position zero first contains the markup book and later contains the events book, React may associate state at that keyed position with a different item. Stable IDs preserve the intended item identity through filtering and reordering. Generating a random key on each render has the opposite effect: it gives the item a new identity every time.
In the shelf, selection belongs to the parent and filtering does not change selectedId. Hiding a selected book with a search therefore retains the parent's choice. Clearing the search makes its description visible again. If an entry owned its own state instead, removing that entry from the rendered tree would unmount it, and adding it later would initialise a new instance.
Read class components and lifecycle work
Function components and class components express the same broad separation between inputs, retained state and rendering. A class extends Component, reads this.props, initialises this.state, and updates state with this.setState. Its render method returns the interface description. For example:
import { Component } from "react";
class PageCounter extends Component {
state = { pages: 0 };
recordPage = () => {
this.setState(previous => ({ pages: previous.pages + 1 }));
};
render() {
return (
<button type="button" onClick={this.recordPage}>
Pages recorded: {this.state.pages}
</button>
);
}
}
The arrow-function field retains the instance's this. A normal method used as a callback needs suitable binding if it accesses the instance. Class setState shallowly merges its object update; a useState setter replaces the value for that Hook. When constructing class state in a constructor that reads props, call super(props) before using this.
Mounting adds a component to the tree, updating supplies changed props or state, and unmounting removes it. Class lifecycle methods include componentDidMount, componentDidUpdate and componentWillUnmount. Older componentWillMount, componentWillReceiveProps and componentWillUpdate names describe legacy lifecycle APIs; they are unsuitable starting points for new component code.
Function components use Effects when they need to synchronise an external system. A timer gives a small local example:
Worked example
Trace a timer from mounting to unmounting
Consider a fresh ReadingClock whose state starts at zero. First follow its normal lifecycle without the additional development Strict Mode cycle:
import { useEffect, useState } from "react";
function ReadingClock() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setSeconds(previous => previous + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return <p>Elapsed timer ticks: {seconds}</p>;
}
Call the interval created by setup A. The updater avoids capturing a stale seconds value, so a tick applies to the preceding pending state.
| Moment | State and displayed result | Effect and interval |
|---|---|---|
| Initial render and commit | seconds is 0; the paragraph displays 0. | Setup has not yet created an interval. |
| Effect setup | State and text remain 0. | Setup creates interval A. |
| First tick, update and commit | The updater changes 0 to 1; the paragraph displays 1. | The unchanged [] dependencies keep A running. |
| Another committed render with unchanged state | The paragraph still displays 1. | No new interval is created. |
| Unmount | The paragraph is removed. | Cleanup calls clearInterval for A; no interval remains. |
The empty dependency array is appropriate because setup reads no changing props or state. If the dependency argument were omitted, the Effect would clean up and set up again after each committed render of this component, including a tick-driven update. With cleanup intact, it would keep one interval but repeatedly restart its schedule. An Effect that reads changing props or state instead needs those reactive values reflected in its dependencies.
With Strict Mode enabled at the application root in development, the initial Effect also undergoes a setup → cleanup → setup check. Interval A is cancelled before its replacement is started, leaving one active interval. This checks that setup and cleanup balance; it does not call for a second permanent timer. Timer ticks also need not equal precise wall-clock seconds when browser scheduling is delayed.
Exercises
Checkpoint
Exercise 1: predict two queued replacements
A handler from a render where pages is 7 calls setPages(pages + 1) twice. What replacement value is requested? Rewrite the handler to record two increments based on the preceding pending value.
Solution · Exercise 1 solution
Both calls compute 7 + 1, so both request 8. Use setPages(previous => previous + 1) twice to queue two increments, producing 9. The updater's parameter represents the preceding pending state; the captured pages belongs to the handler's render.
Checkpoint
Exercise 2: explain a frozen search field
A component renders <input value={query} />, where query is state initially set to an empty string. Why does typing fail to produce an editable state-driven field? Supply the missing prop.
Solution · Exercise 2 solution
The component controls the displayed value but supplies no handler to update it. Add onChange={event => setQuery(event.target.value)}. Every edit then requests the state that the next render supplies back as value.
Checkpoint
Exercise 3: preserve book identity
Three books have IDs markup, events and layout. A filter removes the first. Explain what a key based on the array index would identify before and after filtering, and choose an appropriate key.
Solution · Exercise 3 solution
Index zero initially identifies the markup position and then identifies the events position. That can preserve component state for the position rather than for the intended book. Use key={book.id}; the events book keeps the same ID before and after filtering.
Checkpoint
Exercise 4: find the owner and the cleanup
The layout book is selected, then a search hides it, then the search is cleared. Explain why the shelf retains the selection. Separately, what happens if a timer is created during every render without being cancelled?
Solution · Exercise 4 solution
selectedId belongs to the shelf, and the search changes only query. The selected ID therefore remains layout while the entry is hidden and supplies selected=true when it reappears. A timer created during every render accumulates independent intervals. A timer Effect should pair its setup with cleanup so that stopped synchronisation leaves no active interval behind.
The same state-and-event reasoning begins with ordinary browser objects in DOM events and object state.