React Fundamentals: Components, JSX & State
Build your first React UI from scratch. Learn how components compose, how JSX maps to the DOM, and how props and useState let you create interactive interfaces without fighting the framework.
4 sections · ~25 min · 5-question quiz (pass ≥ 70%)
1Components and JSX: Building Blocks of the UI
React applications are trees of components — reusable functions (or classes) that describe what should appear on screen. A component is just a function that returns JSX:
function Welcome({ name }) {
return <h1>Hello, {name}!</h1>;
}
function App() {
return (
<main>
<Welcome name="Ada" />
<Welcome name="Grace" />
</main>
);
}
JSX looks like HTML but is JavaScript syntax sugar. The compiler transforms it into React.createElement calls. Three rules to internalize early:
- One root element per return. Wrap siblings in a fragment (
<>...</>) if you don't want an extra DOM node. - JavaScript expressions go inside
{ }. Variables, function calls, ternaries — anything that evaluates to a value. - Use
classNameinstead ofclass, andhtmlForinstead offor, because JSX is JavaScript, not HTML.
Components compose like LEGO: small, focused pieces that you nest to build complex UIs. Name them with PascalCase (UserCard, not userCard) so React can distinguish components from native HTML tags.
2Props: Passing Data Down the Tree
Props (short for properties) are how parent components pass read-only data to children. Think of them as function arguments — the child receives them and renders accordingly, but cannot modify them.
function Avatar({ src, alt, size = 48 }) {
return (
<img
src={src}
alt={alt}
width={size}
height={size}
style={{ borderRadius: "50%" }}
/>
);
}
function UserCard({ user }) {
return (
<article>
<Avatar src={user.avatarUrl} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.role}</p>
</article>
);
}
Key conventions:
- Props flow one direction — parent → child. If a child needs to communicate back, you'll use callbacks (covered in the events section).
- Destructure in the parameter list for cleaner code. Default values (
size = 48) work just like regular function defaults. - Spread props when forwarding attributes:
<input {...fieldProps} />passes every key as an individual prop.
Props are immutable inside the child. If UserCard tried user.name = "Hacked", React would warn in strict mode — and more importantly, your UI would become unpredictable.
3State with useState: Making Components Interactive
Props describe external data; state describes data a component owns and can change over time. The useState hook gives you a value and a setter:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount((c) => c + 1)}>Also increment</button>
</div>
);
}
When you call setCount, React schedules a re-render with the new value. Important rules:
- Never mutate state directly.
count++oruser.name = "x"won't trigger a re-render. Always call the setter. - Updates may be batched. If several setters fire in one event handler, React merges them into a single render.
- Functional updates (
setCount(c => c + 1)) are safer when the new value depends on the previous one — especially inside callbacks or effects.
State is local to the component that declares it. Two <Counter /> instances on the same page maintain independent counts. When multiple components need the same state, you'll lift it up to a common ancestor — a pattern covered in the intermediate track.
4Events, Lists, Keys & Conditional Rendering
React events use camelCase names and receive a synthetic event object:
function SearchForm({ onSearch }) {
const [query, setQuery] = useState("");
function handleSubmit(e) {
e.preventDefault();
onSearch(query.trim());
}
return (
<form onSubmit={handleSubmit}>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button type="submit">Search</button>
</form>
);
}
Rendering lists — map an array to elements and give each a stable key:
function TodoList({ todos, onToggle }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
</label>
</li>
))}
</ul>
);
}
Keys help React match list items across renders. Use stable, unique IDs — never the array index if items can be reordered or deleted.
Conditional rendering patterns you'll use daily:
{isLoggedIn ? <Dashboard /> : <LoginPrompt />}
{error && <p className="error">{error}</p>}
{items.length === 0 ? <EmptyState /> : <ItemGrid items={items} />}
Prefer early returns for complex conditions (if (!data) return <Spinner />) to keep the happy path readable.