React Core Fundamentals: JSX, Components, State & Props
Master React's foundational concepts — JSX, Virtual DOM, class vs functional components, props, state, and lifecycle — with real-world examples from production apps.
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.
Enjoyed this article?
Explore the Frontend Engineering learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.