Components: Building Blocks of a React UI
Understand what a React component is, how to name and structure function components, how to draw sensible component boundaries, and the difference between controlled and uncontrolled components.
Every React UI, no matter how large, is assembled from components — small, self-contained functions that return what should appear on screen. Learning to write a component well is less about syntax and more about judgment: where do you draw the line between one component and the next, what does a component own, and what does it hand off to its caller? This lesson builds that judgment from the ground up, starting with the plain function component and ending with the controlled-vs-uncontrolled distinction that shapes almost every form you will ever build.
Function components and naming
In modern React, a component is simply a JavaScript (or TypeScript) function that returns JSX describing what should render. React recognizes a function as a component by convention, not magic: its name must start with a capital letter. function button() is treated as a plain function and its JSX-like return value is never rendered as a component; function Button() is recognized as a component and can be used as <Button />. This single naming rule is why every component you write, without exception, gets PascalCase — UserCard, NavBar, LoginForm — never camelCase or lowercase.
Beyond satisfying the compiler, a good component name documents intent. Card tells the next developer what kind of thing they are looking at before they read a single line of its body; Component1 or Thing tells them nothing. Name components after the role they play in the UI (PriceTag, SearchResults) rather than after their implementation details (DivWithFlex), because the name should stay accurate even if the internals change.
TypeScript adds one more reason naming matters: the props type for a component is conventionally named <ComponentName>Props, so a well-named component automatically produces a well-named, easy-to-find type — ButtonProps, UserCardProps — that shows up right next to it in autocomplete and in your editor's 'go to definition'.
Drawing component boundaries
The hardest skill in component design isn't writing a component — it's deciding where one component ends and the next begins. A boundary is well drawn when the component has a single, nameable responsibility: Avatar renders a user's picture, Rating renders a star score, ProductCard composes Avatar, Rating, and a price into one unit. A boundary is poorly drawn when a component tries to do everything (a 400-line Page component that fetches data, renders a header, a form, and a footer all in one function) or when it is split so finely that using it requires threading a dozen unrelated props through three layers just to reach the piece that actually needs them.
A useful heuristic: if you find yourself describing a chunk of JSX with 'and' — 'this renders the header and the search bar and the results list' — that's usually a sign it should be three components, not one. Conversely, if two pieces of markup are always used together, always change together, and never appear apart, keeping them in one component is often simpler than splitting them.
Reusability is a consequence of good boundaries, not a goal you chase directly. A component becomes reusable when it depends only on its own props and not on assumptions about where it happens to be rendered — no reaching into a specific parent's layout, no hardcoded page-specific text. Write the component for the one place you need it first; extract it into a shared, reusable version only once a second, genuinely similar use case appears.
• One component, one clear responsibility — describable in a single short sentence. • Split when a section has 'and' in its description; merge when pieces always change together. • Reusable components avoid hardcoded assumptions about their parent or page.
Presentational vs. container awareness
A useful (if informal) way to categorize components is by how much they know about *data* versus how much they know about *appearance*. A presentational component receives everything it needs through props and focuses purely on rendering — a PriceTag that takes a amount: number prop and formats it doesn't know or care where that number came from. A container component, by contrast, is concerned with *getting* data — calling a hook, reading context, managing state — and then handing the results down to presentational components as props.
This isn't a rigid rule enforced by React itself; it's a mental model that keeps components testable and reusable. A presentational component can be rendered in isolation with fake props and look identical to production, which makes it trivial to reuse in a different context or test without mocking a network call. Container components tend to be page-specific and non-reusable, and that's fine — their job is to plumb data into the reusable, presentational pieces.
In practice most real components sit somewhere between the two extremes, and that's normal. The value of the distinction is in the question it prompts when a component starts feeling awkward to reuse or test: 'is this component doing data work that belongs one level up?'
Component identity and controlled vs. uncontrolled components
React decides whether to preserve or discard a component's internal state by comparing its *type* and *position* in the tree between renders — this is what 'component identity' means. If the same component type renders in the same slot across two renders, React treats it as the same component instance and keeps its state; if the type changes (or a key changes), React unmounts the old instance and mounts a fresh one, resetting all of its state. Understanding this identity model explains a huge class of 'why did my state reset?' bugs, and it becomes critical once you study useState in depth later in this module.
A distinct but related idea is whether a form element's value lives in React state (a *controlled* component) or in the DOM itself (an *uncontrolled* component). A controlled <input> has its value set from a state variable and updates that state on every onChange, so React is the single source of truth for what the input displays — you can validate, transform, or reject a keystroke on every render. An uncontrolled <input> has no value prop; the browser DOM owns the current text, and you read it out only when you need it, typically via a ref.
Neither approach is universally 'better.' Controlled inputs are the default choice for most application forms because they make validation, conditional disabling, and syncing multiple fields straightforward — the render always reflects the true current value. Uncontrolled inputs are lighter-weight and preferable for simple forms, file inputs (which cannot be controlled at all, since browsers won't let JavaScript set a file input's value), or when integrating a non-React widget that wants to manage its own DOM state.
A function component, controlled and uncontrolled inputs
A controlled input always has both value and onChange; supplying value without onChange makes React warn and effectively freeze the field. An uncontrolled input uses defaultValue (not value) to set its initial text without React tracking it afterward.
// A minimal presentational component
type PriceTagProps = { amount: number; currency?: string };
function PriceTag({ amount, currency = "USD" }: PriceTagProps) {
return <span>{currency} {amount.toFixed(2)}</span>;
}
// Controlled input: React state is the source of truth
function ControlledSearch() {
const [query, setQuery] = useState("");
return (
<input value={query} onChange={(e) => setQuery(e.target.value)} />
);
}
// Uncontrolled input: the DOM owns the value, read via a ref
function UncontrolledSearch() {
const inputRef = useRef<HTMLInputElement>(null);
function handleSubmit() {
console.log(inputRef.current?.value);
}
return (
<>
<input ref={inputRef} defaultValue="" />
<button onClick={handleSubmit}>Search</button>
</>
);
}Code Examples
Greeting is a minimal presentational component: it takes one prop and returns JSX with no internal state or side effects, which makes it trivially reusable and testable. Expected result: Renders: <h1>Hello, Asha!</h1>
LoginForm is a controlled form: both fields are driven entirely by state, so the component always knows the exact current value of each field and can validate or transform it before calling onSubmit — a container component higher up owns what actually happens with the credentials.
value={email} onChange={(e) => setEmail(e.target.value)}
function Greeting({ name }: { name: string }) {
return <h1>Hello, {name}!</h1>;
}
// used elsewhere
<Greeting name="Asha" />;type LoginFormProps = { onSubmit: (email: string, password: string) => void };
function LoginForm({ onSubmit }: LoginFormProps) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
onSubmit(email, password);
}
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
<input value={password} onChange={(e) => setPassword(e.target.value)} type="password" placeholder="Password" />
<button type="submit">Log in</button>
</form>
);
}Presentational component composed inside a container
The container knows how to get data; the presentational components only know how to display whatever props they receive.
UserProfilePage (container)
|-- fetches user data (state/hooks)
|
+--> <Avatar url={user.avatarUrl} /> (presentational)
+--> <Rating value={user.rating} /> (presentational)
+--> <PriceTag amount={user.balance} /> (presentational)Important Rules
Component names must start with a capital letter, or React treats them as plain HTML tags/functions, not components.
Give a component a single, nameable responsibility; split it when its description needs 'and'.
A controlled input always sets value from state and updates that state in onChange; never set value without a matching onChange.
An uncontrolled input uses defaultValue/defaultChecked and is read via a ref, not value.
React resets a component's internal state when its type or key changes at a given position, even if the surrounding markup looks identical.
Where this shows up
Design systems are built almost entirely from small, well-bounded presentational components — Button, Input, Card, Badge — each accepting a tightly typed props API, while page-level container components fetch data and wire those primitives together. Getting component boundaries right early is what lets a team scale a codebase from a handful of screens to hundreds without every change rippling across unrelated files.
function userCard(props) { return <div>{props.name}</div>; } function UserCard({ name }: UserCardProps) { return <div>{name}</div>; }
A lowercase function name is not recognized as a component by JSX; <userCard /> would be parsed as an unknown HTML tag, not an invocation of this function.
<input value={query} /> // no onChange <input value={query} onChange={(e) => setQuery(e.target.value)} />
A controlled input with value but no onChange becomes effectively read-only and React logs a warning, because nothing ever updates the state that drives the field.
[multiple-choice] Why must a React component's name start with a capital letter? A. It's just a style preference with no functional effect B. JSX uses capitalization to distinguish a component from a built-in HTML tag C. TypeScript requires all functions to be capitalized D. Lowercase names cause a runtime crash
B. JSX uses capitalization to distinguish a component from a built-in HTML tag — JSX treats a lowercase tag (<div>) as a built-in HTML element and a capitalized tag (<Button>) as a reference to a component function in scope — lowercase component functions are never recognized as components.
[multiple-choice] What makes an <input> a 'controlled' component in React? A. It has an onClick handler B. Its value is set from React state and updated via onChange C. It uses a ref to read its text D. It is wrapped in a <form> element
B. Its value is set from React state and updated via onChange — A controlled input's displayed value is always driven by a React state variable, with onChange updating that state on every keystroke, making React the single source of truth for the field's value.
[true-false] True or False: A file input (<input type="file">) can be fully controlled by setting its value from React state. A. True B. False
B. False — Browsers do not allow JavaScript to set a file input's value for security reasons, so file inputs are always uncontrolled and must be read via a ref.
Refactor a single 'SearchBar' component that currently both fetches search results (via a hook) and renders the input and results list into two components: a container SearchBarContainer that manages state and fetching, and a presentational SearchInput that only renders an <input> and calls a callback prop on change.
type SearchInputProps = { value: string; onChange: (v: string) => void }; function SearchInput({ value, onChange }: SearchInputProps) { return <input value={value} onChange={(e) => onChange(e.target.value)} />; } function SearchBarContainer() { const [query, setQuery] = useState(""); const results = useSearchResults(query); // some data-fetching hook return ( <div> <SearchInput value={query} onChange={setQuery} /> <ResultsList results={results} /> </div> ); }
SearchInput's props should be just { value: string; onChange: (v: string) => void } — it should not know that fetching exists.