React Development · Lesson 2 of 15
Core Fundamentals
What React Actually Is
React is a UI library — not a framework. It renders a tree of components and re-renders only what changed. That's the whole contract.
Everything else — routing, state management, data fetching — you choose separately. This is React's biggest strength and most common source of confusion for newcomers.
JSX: JavaScript + HTML, Explained Properly
JSX looks like HTML but compiles to JavaScript function calls.
// What you write
const button = <button className="btn" onClick={handleClick}>Save</button>;
// What Babel compiles it to
const button = React.createElement(
"button",
{ className: "btn", onClick: handleClick },
"Save"
);Key JSX rules that trip people up:
// 1. className, not class
<div className="container">
// 2. Expressions in {}, not {{ }}
<span>{user.name}</span>
<span>{isAdmin ? "Admin" : "User"}</span>
// 3. Self-closing tags are mandatory
<img src={logo} alt="Logo" />
<br />
// 4. Single root element (or Fragment)
return (
<>
<Header />
<Main />
</>
);
// 5. camelCase event handlers
<input onChange={handleChange} onKeyDown={handleKey} />Real-World JSX: Dashboard Card Component
function MetricCard({ title, value, trend, icon: Icon }) {
const isPositive = trend > 0;
return (
<div className={`card ${isPositive ? "card--positive" : "card--negative"}`}>
<div className="card__header">
<Icon size={20} />
<span className="card__title">{title}</span>
</div>
<div className="card__value">{value}</div>
<div className="card__trend">
{isPositive ? "▲" : "▼"} {Math.abs(trend)}%
</div>
</div>
);
}
// Usage
<MetricCard
title="Monthly Revenue"
value="$42,800"
trend={12.5}
icon={DollarSign}
/>The Virtual DOM: Why React Is Fast
React keeps a virtual copy of the DOM in memory. When state changes:
- React creates a new virtual DOM tree
- Diffs it against the previous tree (reconciliation)
- Computes the minimum set of real DOM changes
- Applies only those changes (commit phase)
State Change
↓
New Virtual DOM Tree
↓
Diff with Previous Virtual DOM ← This is cheap (pure JS objects)
↓
Minimal Real DOM Updates ← This is expensive (avoided when possible)Why this matters in practice:
// Without React: browser updates the entire list on every keystroke
document.getElementById("list").innerHTML = items.map(renderItem).join("");
// With React: only changed items re-render
function ItemList({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li> // key helps React identify changes
))}
</ul>
);
}The key prop is how React matches elements between renders. Use stable, unique IDs — never array indices for dynamic lists.
Functional vs Class Components
Use functional components. Class components are legacy. Here's why:
// Class component (legacy, avoid for new code)
class UserGreeting extends React.Component {
constructor(props) {
super(props);
this.state = { greeting: "Hello" };
}
componentDidMount() {
// fetch data here
}
render() {
return (
<h1>
{this.state.greeting}, {this.props.name}!
</h1>
);
}
}
// Functional component (modern, preferred)
function UserGreeting({ name }) {
const [greeting, setGreeting] = useState("Hello");
useEffect(() => {
// fetch data here
}, []);
return <h1>{greeting}, {name}!</h1>;
}Functional components are:
- Shorter and easier to read
- Easier to test
- Better for performance optimizations
- The only way to use hooks
Props: The Component API
Props are how parent components talk to children. Treat them as read-only.
// Define clear prop types in TypeScript
interface UserCardProps {
name: string;
email: string;
role: "admin" | "editor" | "viewer";
avatar?: string; // Optional
onDelete: (id: string) => void; // Callback prop
}
function UserCard({ name, email, role, avatar, onDelete }: UserCardProps) {
return (
<div className="user-card">
<img
src={avatar ?? "/default-avatar.png"}
alt={`${name}'s avatar`}
/>
<div className="user-card__info">
<h3>{name}</h3>
<p>{email}</p>
<span className={`badge badge--${role}`}>{role}</span>
</div>
<button onClick={() => onDelete(email)}>Remove</button>
</div>
);
}Prop Patterns You'll Use Daily
// 1. Children prop — composable components
function Card({ children, className = "" }) {
return (
<div className={`card ${className}`}>
{children}
</div>
);
}
// 2. Spread props — forwarding attributes
function Input({ label, error, ...inputProps }) {
return (
<div>
<label>{label}</label>
<input {...inputProps} className={error ? "input--error" : ""} />
{error && <span className="error-text">{error}</span>}
</div>
);
}
// 3. Render prop — inversion of control
function DataFetcher({ url, render }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then((r) => r.json())
.then((data) => { setData(data); setLoading(false); });
}, [url]);
return render({ data, loading });
}
// Usage
<DataFetcher
url="/api/users"
render={({ data, loading }) =>
loading ? <Spinner /> : <UserList users={data} />
}
/>State: Component Memory
State is data that, when changed, causes the component to re-render.
// Correct: updating state immutably
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: "Learn React", done: false },
{ id: 2, text: "Build something", done: false },
]);
const toggleTodo = (id) => {
// Never mutate state directly
// BAD: todos[0].done = true;
// GOOD: create new array with updated item
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
};
const addTodo = (text) => {
setTodos((prev) => [
...prev,
{ id: Date.now(), text, done: false },
]);
};
const deleteTodo = (id) => {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
};
return (
<div>
<AddTodoForm onAdd={addTodo} />
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))}
</div>
);
}State Batching (React 18+)
React 18 batches all state updates automatically, even in async code:
function handleSave() {
// React 18: these three updates cause ONE re-render
setName("Alice");
setAge(30);
setLoading(false);
// In async functions — also batched in React 18
setTimeout(() => {
setCount((c) => c + 1); // React 18: still batched
setActive(true); // ONE re-render
}, 1000);
}When to Use Local State vs Lifted State
// Local state: only this component needs it
function SearchInput() {
const [query, setQuery] = useState("");
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
);
}
// Lifted state: multiple siblings need the same data
function FilterableTable() {
const [filter, setFilter] = useState(""); // ← lifted here
return (
<>
<FilterInput value={filter} onChange={setFilter} />
<DataTable data={data} filter={filter} />
</>
);
}Component Lifecycle (in Hooks)
The mental model for functional component lifecycle:
Mount → Render → Commit to DOM → Run effects
Update → Render → Commit to DOM → Cleanup old effects → Run new effects
Unmount → Cleanup effectsfunction UserProfile({ userId }) {
const [user, setUser] = useState(null);
// componentDidMount + componentDidUpdate equivalent
useEffect(() => {
let cancelled = false;
async function loadUser() {
const data = await fetchUser(userId);
if (!cancelled) setUser(data); // prevent setting state after unmount
}
loadUser();
// componentWillUnmount equivalent
return () => {
cancelled = true;
};
}, [userId]); // re-runs when userId changes
if (!user) return <Skeleton />;
return <Profile user={user} />;
}Conditional Rendering Patterns
function Notification({ type, message, count }) {
// 1. Early return (cleaner for loading/error states)
if (!message) return null;
// 2. Ternary for binary states
const icon = type === "error" ? <ErrorIcon /> : <InfoIcon />;
// 3. && for optional elements (watch the falsy 0 trap!)
// BAD: count && <Badge count={count} /> — renders "0" when count is 0
// GOOD:
const badge = count > 0 && <Badge count={count} />;
// 4. Object map for multiple conditions
const colorMap = {
success: "green",
warning: "orange",
error: "red",
info: "blue",
};
return (
<div className={`notification notification--${type}`} style={{ borderColor: colorMap[type] }}>
{icon}
<span>{message}</span>
{badge}
</div>
);
}Lists and Keys
// Real-world: a data table with sorting and selection
function DataTable({ rows, columns }) {
const [selected, setSelected] = useState(new Set());
const toggleRow = (id) => {
setSelected((prev) => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
};
return (
<table>
<thead>
<tr>
{columns.map((col) => (
<th key={col.key}>{col.label}</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr
key={row.id} // always unique, stable ID
className={selected.has(row.id) ? "row--selected" : ""}
onClick={() => toggleRow(row.id)}
>
{columns.map((col) => (
<td key={col.key}>{row[col.key]}</td>
))}
</tr>
))}
</tbody>
</table>
);
}Error Boundaries
Error boundaries catch JavaScript errors in any component below them in the tree.
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
// Log to error tracking (Sentry, Datadog, etc.)
errorTracker.capture(error, {
componentStack: info.componentStack,
});
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Something went wrong</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
// Wrap route sections, not individual components
function App() {
return (
<ErrorBoundary>
<Header />
<ErrorBoundary fallback={<DashboardError />}>
<Dashboard />
</ErrorBoundary>
<Footer />
</ErrorBoundary>
);
}Interview Questions Answered
Q: What is the difference between state and props?
Props are inputs passed from parent to child — immutable from the child's perspective. State is internal data managed by the component itself that can change over time, triggering re-renders.
Q: Why must React state updates be immutable?
React compares previous and next state by reference (===). If you mutate an object directly, the reference stays the same, React sees no change, and skips the re-render. Always return new objects/arrays.
Q: When does a component re-render?
A component re-renders when: (1) its own state changes, (2) its parent re-renders (and it's not memoized), (3) a context it consumes changes.
Q: What is the key prop for?
key is React's way to identify which elements changed between renders in a list. Stable, unique keys help React reuse DOM nodes efficiently rather than destroying and recreating them. Never use array index as key for lists that can reorder or filter.