A hands-on introduction to React for students who already know C++ and Python. Learn to think declaratively, build components, manage state, and compose real UIs — all live in your browser.
Learning objective: After this step you will be able to explain the difference between imperative and declarative UI programming, and modify a simple React component.
You know how to manipulate the DOM the imperative way — you tell the browser how to do it, step by step:
// Imperative: You write the HOW
const h1 = document.getElementById('greeting');
h1.textContent = 'Hello, CS 130!';
h1.style.color = '#2774AE';
React asks you to think declaratively — you describe what the UI should look like for a given moment, and React figures out the minimal DOM updates needed to get there:
// Declarative (React): You describe the WHAT
function App() {
return <h1 className="greeting">Hello, CS 130!</h1>;
}
| Imperative (Vanilla JS / C++) | Declarative (React) | |
|---|---|---|
| Mindset | How to reach the state | What the state should look like |
| Analogy | Turn-by-turn GPS directions | Dropping a pin on the destination |
| DOM updates | You call element.textContent = ... |
React diffs the Virtual DOM and patches only what changed |
| Bugs | Easy to forget a step, leaving stale UI | React re-renders the whole component; inconsistent state is much harder |
The declarative mindset feels strange at first — you are used to telling the computer exactly what to do, step by step. In React, you describe the destination and let React figure out the route. This shift takes time. If it feels unnatural, that is a sign you are learning something fundamentally new, not that you are doing it wrong. Every React developer went through this disorientation.
React’s JSX uses the same tags as HTML. Here are the ones you will see throughout this tutorial:
| Tag | Purpose | Example |
|---|---|---|
<h1> – <h6> |
Headings (h1 = largest) | <h1>Hello!</h1> |
<p> |
Paragraph of text | <p>Welcome to React.</p> |
<div> |
Generic container (no visual meaning) | <div>...</div> |
<span> |
Inline container (for styling a word or phrase) | <span>Sale!</span> |
<button> |
Clickable button | <button>Click me</button> |
<ul>, <li> |
Unordered list and list items | <ul><li>Item</li></ul> |
<img> |
Image (self-closing) | <img src="photo.jpg" /> |
These tags describe structure — what each piece of content is. They say nothing about how it looks. That is the job of CSS.
CSS (Cascading Style Sheets) controls how elements look — colors, spacing, fonts, borders, and layout. A CSS class is a reusable set of styles that you apply to elements by name:
.greeting { color: tomato; font-size: 24px; }
In React, you attach a CSS class with the className prop (not class — that is a reserved JavaScript keyword):
<h1 className="greeting">Hello!</h1>
This tutorial loads Bootstrap (a CSS library) automatically, so layout and typography are handled for you. The styles.css file is for your own custom styles. You do not need to learn CSS for this tutorial — styling is provided in every step after this one. Here, you will make one small change to get comfortable with the idea.
The <h1>...</h1> syntax inside JavaScript is called JSX. It looks like HTML, but it is not — Babel compiles it to React.createElement(...) calls that build a lightweight JavaScript object tree (the Virtual DOM). You will learn the details and rules of JSX in the next step.
Before changing anything, look at the App component. Predict: what does {name} inside the JSX evaluate to? What does className="greeting" connect to in styles.css? Write your predictions, then read on.
The preview shows a greeting component. Make two changes:
App.jsx: Change "World" to your own name in the name variablestyles.css: Change the color from tomato to #2774AE (or any other color)The preview rebuilds automatically when you save (Ctrl+S). Use ↻ Refresh if needed.
.greeting {
color: tomato; /* Task 2: Change this color */
}
function App() {
const name = "World"; // Task 1: Change this to your name
return (
<div className="p-4">
<h1 className="greeting display-6 fw-bold">
Hello, {name}!
</h1>
<p className="mt-2 text-secondary">Welcome to React.</p>
</div>
);
}
// Mount — you don't need to change this
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
1. In vanilla JS you’d write h1.textContent = newTitle to update a heading. What is the declarative React equivalent?
React’s mental model: you change the data, not the DOM. React calls your component function, builds a new Virtual DOM tree, diffs it against the previous one, and patches only what changed. You describe what the UI looks like for a given set of data; React figures out how to get there. In Step 4 you will learn about useState, which makes data changes automatically trigger this cycle.
2. What does Babel compile <h1>Hello</h1> into?
JSX is syntactic sugar. Babel transforms it to React.createElement(type, props, ...children) calls, which return plain JavaScript objects — the Virtual DOM. No real DOM nodes are created at this stage. React’s reconciler does that later, and only for the parts that actually changed.
3. A teammate proposes: “Instead of learning React, let’s just use vanilla JavaScript with document.getElementById — it’s more direct and we already know it.”
Evaluate this suggestion for a project with 50+ interactive UI components that update frequently.
This is a real trade-off. For a static page or 2-3 interactive widgets, vanilla JS is perfectly fine and simpler. But as interactivity scales, manually synchronizing data and DOM becomes the #1 source of bugs. React’s value proposition is eliminating that synchronization — you declare what the UI looks like for each state, and React handles the rest.
Learning objective: After this step you will be able to identify and fix common JSX syntax errors, and explain why JSX differs from HTML.
In C++ and Python you build programs by composing functions. React works the same way, but functions return JSX (UI) instead of numbers or strings.
// SUB-GOAL: Define the component as a function returning JSX
// Python function: React component:
def greet(name): function Greet({ name }) {
return f"Hello, {name}" return <p>Hello, {name}!</p>;
}
Components let you split a complex UI into small, reusable pieces — exactly like how you extract a C++ helper function to avoid repeating code.
JSX looks like HTML but is actually JavaScript. These four rules catch most beginners:
| Rule | Wrong (HTML instinct) | Correct (JSX) |
|---|---|---|
| CSS class attribute | class="..." |
className="..." (class is a JS keyword) |
| Self-closing tags | <img src={u}> |
<img src={u} /> (required in JSX) |
| Inline style | style="color:red" |
style={{color: 'red'}} (JS object, not CSS string; prefer CSS classes when possible) |
| Multiple root elements | return <h1/><p/> |
return <><h1/><p/></> (single root required) |
| Component names | <card /> |
<Card /> (must be capitalized) |
| Embed JS expressions | <p>name</p> |
<p>{name}</p> (curly braces for expressions) |
Before fixing the bugs below: look at the Badge component’s style prop. It says style="background: color;". Predict: what is wrong with this syntax? Write your prediction, then fix it.
The file below has three bugs that prevent it from rendering correctly.
App.jsx (hint: use the table above)<Badge> below the existing two, with a label of your choice and a different colorThe Badge component is already defined — you just need to use it.
// A reusable Badge component
// Props: label (string), color (string — any CSS color)
function Badge({ label, color }) {
return (
<span className="badge rounded-pill fw-semibold" style="background: color;">
{label}
</span>
);
}
function App() {
return (
// BUG: Multiple root elements without a wrapper
<h1 class="h3 mb-3">My Badges</h1>
<div className="d-flex gap-2 mt-3">
<Badge label="React" color="#61dafb" />
<Badge label="JavaScript" color="#f7df1e" />
{/* Task: Add a third <Badge> here */}
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
1. Why does React use className instead of class for CSS classes?
JSX is JavaScript, and class is a reserved keyword in JavaScript (used for ES6 classes). Using class inside JSX would cause a syntax error. className maps directly to the DOM property element.className, so it works identically at runtime.
2. Why must JSX components return a single root element?
JSX compiles to React.createElement(...), which returns a single JS object. A function can’t return <A/><B/> any more than it can return 1 2 — only one expression is valid. Wrap siblings in a <div> or the zero-overhead fragment <>...</> (compiles to React.Fragment).
3. How do you write an inline style with font-size: 18px and color: red in JSX?
The JSX style prop takes a JavaScript object, not a CSS string. CSS property names become camelCase (fontSize, not font-size). Values are strings (or numbers for unitless properties). The double braces {{ }} are: the outer {} for a JSX expression, the inner {} for the object literal.
4. Analyze this code. A student writes function card() { return <div>A card</div>; } and uses <card />. It renders an empty box. Why?
React uses the capitalization of a JSX tag to decide: lowercase → HTML element (passes to the browser’s DOM), uppercase → React component (calls your function). <card /> silently becomes an unknown HTML element. The fix: rename to Card and use <Card />.
5. Which of these are correct JSX? (Select all that apply) (select all that apply)
<img ... /> is correct — self-closing tags are required in JSX. <p>{expression}</p> is correct — any JS expression works inside {}. class is a JS reserved word; use className. Browser event handlers use camelCase in JSX: onClick, not onclick.
Learning objective: After this step you will be able to implement components that accept props, and explain why props are read-only.
A component with no props is like a function with no parameters — useful, but limited. Props let you pass data into a component, exactly like calling a function with arguments.
// SUB-GOAL: Define a component that accepts props via destructuring
// C++: void printCard(string name, double price) { ... }
// Python: def render_card(name, price): ...
// React — defining the component:
function ProductCard({ name, price }) {
return (
<Card>
<Card.Body>
{/* SUB-GOAL: Use props to render dynamic content */}
<h3>{name}</h3>
<p>${price.toFixed(2)}</p>
{/* TODO: Add more elements here */}
</Card.Body>
</Card>
);
}
// SUB-GOAL: Use the component with specific prop values
<ProductCard name="Laptop" price={999.99} />
<ProductCard name="Mouse" price={29.99} />
The { name, price } syntax in the function signature is called destructuring — it unpacks properties from the props object into separate variables. If you have used C++17 structured bindings, it works the same way:
C++: const auto [name, price] = product; // structured binding
Python: name, price = product // tuple unpacking
React: function Card({ name, price }) { ... } // destructuring
const function parameters in C++)title="Hello"). All other types — numbers, booleans, expressions — use braces: price={99.99}, active={true}, onClick={handleClick}&&Task 4 below asks you to show a badge only when onSale is true. In C++ or Python, you would use an if statement. But JSX is an expression (it produces a value), not a block of statements — you cannot write if inside it, just like you cannot write if inside cout << ... or an f-string.
Instead, React uses JavaScript’s && (logical AND) operator:
{soldOut && <Badge bg="danger">Sold Out!</Badge>}
How it works: JavaScript evaluates the left side first. If soldOut is false, it short-circuits — the right side is never evaluated, and React renders nothing (because false is ignored in JSX). If soldOut is true, JavaScript returns the right side, and React renders the Badge.
This is the React equivalent of:
# Python — you can't embed if-statements in f-strings either
sale_text = "Sale!" if on_sale else ""
You will learn more conditional rendering patterns (ternary, early return) in Step 6.
Before writing any code, predict: what will the ProductCard look like when onSale is true vs false? Now that you know the && pattern, write the JSX in your head, then implement it.
The ProductCard component skeleton is provided. Complete it so that it:
name as an <h3>price formatted to two decimal places (use price.toFixed(2))description in a <p> tagonSale is trueThe App function already passes the right props — you only need to build the card.
Challenge (optional): After passing the tests, add a third ProductCard in App with your own product data and onSale value. Notice how the same component renders differently based on the data you pass — that is the power of props.
const { Card, Badge } = ReactBootstrap;
function ProductCard({ name, price, description, onSale }) {
// Task: Build the card UI using the four props above.
// Requirements:
// 1. <h3> showing name
// 2. Price formatted to 2 decimal places
// 3. <p> showing description
// 4. A "Sale!" badge (shown only if onSale is true)
//
// Hint: Use <Badge bg="danger">Sale!</Badge> for the badge
return (
<Card className="product-card">
<Card.Body>
{/* Your code here */}
</Card.Body>
</Card>
);
}
function App() {
return (
<div className="p-4 d-flex gap-4 flex-wrap">
<ProductCard
name="Mechanical Keyboard"
price={129.99}
description="Tactile switches, RGB backlit, compact 75% layout."
onSale={true}
/>
<ProductCard
name="USB-C Hub"
price={49.99}
description="7-in-1 hub: 4K HDMI, 3× USB-A, SD card, 100W PD."
onSale={false}
/>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
1. Inside a component, you have function Card({ title }) { title = 'New'; ... }. What is wrong with this?
Props are immutable inside a component. Mutating them would corrupt the parent’s data and break the predictable top-down data flow that React relies on. If a component needs to change a value, it should use useState (local state) or call a function passed as a prop from the parent.
2. Which of these is the correct way to pass a number prop to a component?
String literals can be passed directly: label="Hello". All other values — numbers, booleans, objects, arrays, functions — must be wrapped in {}: price={99.99}, active={true}, items={[1, 2, 3]}. Without {}, React would interpret 99.99 as a malformed attribute, not a number.
3. Analyze this: <Card title="React" /> and <Card title={"React"} /> produce the same result. When would they differ?
For plain string values, both are equivalent. But {} is required for any JS expression:
title={user.name}, title={isAdmin ? 'Admin' : 'User'}, title={getTitle()}.
Only string literals can use the quote syntax. This is a common source of confusion
for beginners who try price=99.99 (without braces) and get unexpected results.
4. A ProductCard receives price as a prop and renders ${price.toFixed(2)}. What happens if the parent passes price={undefined}?
undefined.toFixed(2) is a runtime error — undefined has no methods.
In production, you would guard against this with a default value:
function ProductCard({ price = 0 }) { ... } or (price ?? 0).toFixed(2).
React does not provide automatic fallbacks for missing props.
Learning objective: After this step you will be able to implement interactive components using
useStateand explain why regular variables don’t trigger re-renders.
This step is where most students get stuck. The idea that changing a variable doesn’t update the UI — and that you need a special React function to do it — feels deeply wrong after years of imperative programming. That confusion is normal and expected. Every React developer had the same “but why doesn’t this just work?” moment.
Before reading further, look at the counter code below. It doesn’t work — clicking +1 does nothing. Spend 2 minutes trying to fix it using what you know from C++ and Python. What approaches did you try? Why didn’t they work?
In C++, a class stores data in member variables that persist across method calls. In React, calling your component function is like constructing a fresh object each time — local variables are reset on every render.
// BROKEN — count is reset to 0 every time the button is clicked
function Counter() {
let count = 0; // ← destroyed on each re-render
return <button onClick={() => count++}>{count}</button>;
}
Understanding why this breaks requires knowing what React does when state changes:
setCount(1)Counter() runs again from the topA let count = 0 at the top of the function is re-executed in step 2, resetting it to 0 every time. The variable does change in memory when you do count++, but React never knows — it has no way to detect that a plain variable changed, so it never triggers step 1.
In C++, you control when member functions execute. In React, you don’t control when your component function runs — React calls it whenever state changes. This means your component must be a pure function of its props and state, with no side effects.
Another instinct that hurts: in C++, vec.push_back(item) modifies the vector in-place and that is perfectly fine. In React, items.push(item) does not trigger a re-render because React compares state by reference equality (===). The array reference hasn’t changed, so React thinks nothing happened. You must create a new array: setItems([...items, item]).
React provides useState to give your component persistent memory:
function Counter() {
// SUB-GOAL: Declare state with an initial value
const [count, setCount] = React.useState(0);
// SUB-GOAL: Define the UI as a function of current state
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
React.useState(initialValue) returns a pair: the current value, and a setter function. Calling the setter triggers a re-render with the new value.
if, for, or nested functionsWhen you write an arrow function inside a component, it captures the current value of variables — just like a C++ lambda with [count] captures by value. If state changes between when the function was created and when it runs, the captured value is stale:
// BUG — both timeouts capture count = 0 at render time
setTimeout(() => setCount(count + 1), 1000); // sets to 1
setTimeout(() => setCount(count + 1), 2000); // also sets to 1 (not 2!)
// FIX — functional update always receives the latest value
setTimeout(() => setCount(prev => prev + 1), 1000); // 0 → 1
setTimeout(() => setCount(prev => prev + 1), 2000); // 1 → 2 ✓
Rule of thumb: Use setCount(prev => prev + 1) (functional form) whenever the new value depends on the old value. Use setCount(5) (direct form) when you know the exact new value.
React does not re-render between setter calls in the same event handler. It batches them and re-renders once at the end. This means multiple direct calls see the same stale value:
function handleTripleClick() {
setCount(count + 1); // count is 0 → sets to 1
setCount(count + 1); // count is still 0 → sets to 1 again!
setCount(count + 1); // count is still 0 → sets to 1 again!
// Result: count goes from 0 to 1, not 0 to 3
}
The functional form fixes this because each call receives the latest pending value, not the stale render-time value:
function handleTripleClick() {
setCount(prev => prev + 1); // 0 → 1
setCount(prev => prev + 1); // 1 → 2
setCount(prev => prev + 1); // 2 → 3 ✓
}
Look at the broken counter code. Predict: when you click the +1 button, does count actually change in memory? If so, why doesn’t the display update? Write your hypothesis before reading the explanation above.
The counter below has two bugs:
let variable instead of useStateTask:
0When something doesn’t update, add a console.log at the top of your component function (before the return):
function Counter() {
const [count, setCount] = React.useState(0);
console.log('Counter rendered, count =', count); // ← appears in browser console on every render
...
}
If the log never appears after a click, the state setter was never called. If it appears but shows the wrong value, check for stale closures. The browser’s React DevTools extension also lets you inspect component state live.
const { Button } = ReactBootstrap;
function Counter() {
// BUG: Using a regular variable — React won't re-render when this changes
let count = 0;
function increment() {
count = count + 1; // BUG: Mutating a local variable has no effect on the UI
console.log('count is now', count); // This logs, but the display never updates!
}
return (
<div className="p-4 text-center">
<h2 className="display-1 mb-4">{count}</h2>
<div className="d-flex gap-2 justify-content-center">
<Button variant="primary" size="lg" onClick={increment}>
+1
</Button>
{/* Task: Add a −1 button and a Reset button */}
</div>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Counter />);
1. Why doesn’t let count = 0; count++; cause the UI to update in React?
React knows nothing about your local variables. The only way to trigger a re-render is to call a state setter from useState. React’s model: setter called → new state value → component function re-executed with new value → DOM diffed and patched. A bare count++ is invisible to React.
2. What is wrong with this code? if (isLoggedIn) { const [user, setUser] = React.useState(null); }
React identifies hooks by their call order, not by name. Every render must call hooks in exactly the same order. If you conditionally call useState, the order changes between renders, and React’s internal array of hook values gets misaligned — causing subtle, hard-to-debug crashes. Always call hooks unconditionally at the top of your component.
3. You have const [items, setItems] = React.useState([]). How do you correctly add an item?
React uses reference equality to detect state changes — if you mutate an array in-place (push, splice) and pass the same reference to setItems, React sees no change and skips the re-render. Always create a new array: [...items, newItem] (append), items.filter(...) (remove), items.map(...) (transform).
4. A teammate proposes storing the counter value in a global variable outside the component instead of using useState, arguing “it’s simpler and doesn’t reset.”
Evaluate this approach — what breaks?
Two problems: (1) React doesn’t know about the global variable, so changing it doesn’t trigger a re-render. (2) Global state is shared across ALL instances of the component — if you render two <Counter /> components, they’d share the same counter. useState is per-instance and triggers re-renders. This is the same reason C++ classes use member variables, not global variables.
5. (Spaced review — Step 2: JSX) A student’s component renders but looks wrong: the heading has no CSS class applied, clicking does nothing, and the image tag causes a syntax error. Which combination of JSX rules is being violated?
Three different JSX rules from Step 2: (1) class → className (reserved keyword),
(2) onclick → onClick (camelCase event handlers), (3) <img> → <img /> (self-closing
tags required in JSX). This question tests whether you can diagnose which rules apply
to specific symptoms — not just recall the rules in isolation.
Learning objective: After this step you will be able to render arrays as lists using
.map()and explain why stablekeyprops are essential for React’s reconciliation.
This step and the next use three JavaScript array methods heavily. If any are unfamiliar, review them here before continuing:
| Method | What it does | Example |
|---|---|---|
.map(fn) |
Transforms each element, returns a new array | [1,2,3].map(x => x * 2) → [2,4,6] |
.filter(fn) |
Keeps elements where fn returns true |
[1,2,3].filter(x => x > 1) → [2,3] |
.reduce(fn, init) |
Combines all elements into one value | [1,2,3].reduce((sum, x) => sum + x, 0) → 6 |
All three return new arrays (or values) — they never mutate the original. This is exactly the pattern React needs.
for Loops to .map()In C++ you’d render a list with a for loop. In React, you use JavaScript’s .map() to transform a data array into an array of JSX elements:
// C++:
for (const auto& task : tasks) { renderTask(task); }
// React:
// SUB-GOAL: Transform data array into JSX array
const taskElements = tasks.map(task =>
<ListGroup.Item key={task.id}>{task.text}</ListGroup.Item>
);
// SUB-GOAL: Render the array inside a container
return <ListGroup>{taskElements}</ListGroup>;
key Prop — React’s Reconciliation HintWhen React re-renders a list, it needs to know which items are stable, added, or removed. Without keys, it compares by position — which causes unnecessary re-renders and subtle UI bugs (like inputs losing focus).
Think of key as a stable identifier, similar to a pointer address or a database primary key:
| Scenario | Without key |
With stable key |
|---|---|---|
| Insert item at start | React re-renders ALL items | React inserts only the new one |
| Delete middle item | Items after the gap get wrong state | React removes only the deleted item |
| Reorder items | State mismatches (e.g. checked checkboxes shift) | Each item keeps its own state |
Never use array index as a key for dynamic lists. If items are reordered or removed, the index changes — defeating the purpose. Use a stable, unique ID.
Before implementing: imagine a list of 3 checkboxes where each has its own checked state. You check the middle one, then delete it. With index-based keys, what happens to the third checkbox’s state? Think it through, then read the key table above.
A task list is partially implemented. Your job:
<ListGroup.Item> with a .map() call over the tasks array<ListGroup.Item> a key prop using task.id (not the index!)task.done using a ternaryChallenge (optional): After passing the tests, add a 7th task to the tasks array (e.g., { id: 7, text: 'Deploy to production', done: false }). Does your .map() handle it automatically without any other code changes? That is the power of data-driven rendering.
const tasks = [
{ id: 1, text: 'Set up React development environment', done: true },
{ id: 2, text: 'Learn about components and JSX', done: true },
{ id: 3, text: 'Understand props and data flow', done: true },
{ id: 4, text: 'Master useState for interactivity', done: false },
{ id: 5, text: 'Render lists with .map() and keys', done: false },
{ id: 6, text: 'Build a real React app', done: false },
];
const { ListGroup } = ReactBootstrap;
function TaskList() {
return (
<div className="p-4 checklist-container">
<h2 className="h4 mb-3">React Learning Checklist</h2>
<ListGroup>
{/* Task: Replace this with a .map() call over tasks */}
<ListGroup.Item>Task goes here</ListGroup.Item>
</ListGroup>
<p className="text-muted small mt-3">
{tasks.filter(t => t.done).length} / {tasks.length} complete
</p>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<TaskList />);
1. Why is it dangerous to use the array index as a key for a dynamic list?
Keys tell React which element is which across re-renders. If item at index 2 is deleted, items at index 3, 4, 5… all shift to 2, 3, 4… React sees those keys as “the same” elements, potentially mismatching stateful inputs (like checked checkboxes or text fields) with the wrong items. Use a stable, unique ID from your data source.
2. You need to render a list of user cards. Which key strategy is correct?
Use key={user.id} — a stable, unique identifier from the data. Avoid: index (breaks with reordering/deletion), Math.random() (changes every render, forcing unmount/remount), and object references (React uses string comparison).
3. (Spaced review — Step 3: Props)
A TaskItem component needs to let the user mark a task as done. The task data comes from the parent via props. Which approach is correct?
Props are read-only — mutating them breaks one-way data flow (option A).
Duplicating props into state (option B) creates a sync risk — the two copies diverge.
Direct DOM manipulation (option D) bypasses React entirely.
The correct pattern: the child calls a callback prop (onToggle), the parent updates
state, and React re-renders with new props. This combines props (Step 3), state (Step 4),
and one-way data flow into a single decision.
Learning objective: After this step you will be able to use conditional rendering patterns (
&&, ternary) in JSX and implement interactive list filtering withuseState.
React uses plain JavaScript conditions inside JSX:
// SUB-GOAL: Show content only when a condition is true
{newMessages > 0 && <span className="badge">{newMessages}</span>}
// SUB-GOAL: Choose between two alternatives
{isComplete ? <span>✓ Done</span> : <span>Pending</span>}
Watch out:
{count && <Badge />}— ifcountis0, React renders the number0, not nothing! Use{count > 0 && <Badge />}instead.
Now you can combine useState (Step 4) with .map() (Step 5) to build interactive, filtered views. A critical principle: store the minimum state and derive everything else.
// BAD — two state variables that must stay in sync
const [allTasks, setAllTasks] = React.useState(tasks);
const [visibleTasks, setVisibleTasks] = React.useState(tasks);
// Bug: if you add a task to allTasks, visibleTasks is stale!
// GOOD — one state variable; visibleTasks is computed fresh every render
const [filter, setFilter] = React.useState('all');
const visibleTasks = allTasks.filter(t => filter === 'all' || t.status === filter);
The good version has a single source of truth (filter). visibleTasks is not state — it is a value derived from state on every render. This eliminates an entire class of sync bugs.
Here is a more complete example:
function FilteredList() {
// SUB-GOAL: Track the current filter in state
const [filter, setFilter] = React.useState('all');
// SUB-GOAL: Derive visible items from data + filter state
const visible = items.filter(item => {
if (filter === 'active') return !item.done;
if (filter === 'done') return item.done;
return true; // 'all'
});
// SUB-GOAL: Render filter controls and filtered list
return (
<div>
<ButtonGroup>
<Button onClick={() => setFilter('all')}>All</Button>
<Button onClick={() => setFilter('done')}>Done</Button>
</ButtonGroup>
<ListGroup>
{visible.map(item =>
<ListGroup.Item key={item.id}>{item.text}</ListGroup.Item>
)}
</ListGroup>
</div>
);
}
Before implementing, predict: if filter state is 'done', which tasks from the data array should be visible? How many items will the .filter() call return?
Add filter functionality to the task list from the previous step:
<Button> components inside the <ButtonGroup>: “All”, “Active”, “Done”useState to track the current filtervariant prop (e.g. variant="primary" for active, variant="outline-secondary" for inactive)const initialTasks = [
{ id: 1, text: 'Set up React development environment', done: true },
{ id: 2, text: 'Learn about components and JSX', done: true },
{ id: 3, text: 'Understand props and data flow', done: true },
{ id: 4, text: 'Master useState for interactivity', done: false },
{ id: 5, text: 'Render lists with .map() and keys', done: false },
{ id: 6, text: 'Build a real React app', done: false },
];
const { Button, ButtonGroup, ListGroup } = ReactBootstrap;
function TaskList() {
const [filter, setFilter] = React.useState('all');
// Task: Filter tasks based on the current filter state
const visibleTasks = initialTasks; // Replace with filtered list
return (
<div className="p-4 checklist-container">
<h2 className="h4 mb-3">React Learning Checklist</h2>
{/* Task: Add filter buttons — "All", "Active", "Done" */}
<ButtonGroup className="mb-3">
{/* Your filter buttons here */}
</ButtonGroup>
<ListGroup>
{visibleTasks.map(task => (
<ListGroup.Item key={task.id}>
{task.done ? '✓' : '✗'} {task.text}
</ListGroup.Item>
))}
</ListGroup>
<p className="text-muted small mt-3">
{initialTasks.filter(t => t.done).length} / {initialTasks.length} complete
</p>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<TaskList />);
1. What does {showBadge && <Badge />} render when showBadge is false?
React ignores false, null, undefined, and true — they render as nothing. {showBadge && <Badge />} works because when showBadge is false, JS short-circuits to false, which React ignores.
2. Analyze this bug: {count && <Badge count={count} />}. When count is 0, a 0 appears in the UI instead of nothing. Why?
JavaScript’s && returns the left operand if it’s falsy. 0 && <Badge /> evaluates to 0.
While false is not rendered by React, 0 IS rendered as the text “0”. The fix:
{count > 0 && <Badge />} — now the left operand is true or false, never 0.
3. Evaluate two approaches to implementing filters:
A: Store the full filtered array in state: const [visibleTasks, setVisibleTasks] = useState(allTasks)
B: Store only the filter string in state: const [filter, setFilter] = useState('all') and derive visible tasks with .filter()
Which is better?
React’s principle: store the minimal state and derive everything else. Storing both the
full list AND a filtered copy creates a sync risk — if items change, you must remember to
update both. With approach B, visibleTasks is always computed fresh from the source of truth.
4. (Spaced review — Step 4: useState) A shopping cart component has this handler:
function handleBuyTwo() {
setCart([...cart, product]);
setCart([...cart, product]);
}
React batches state updates within the same event handler. Both setCart calls capture
the same cart reference (length 1), so both compute [...cart, product] → length 2.
The second call overwrites the first. Fix: use the functional form
setCart(prev => [...prev, product]) — each call receives the latest pending value.
This combines the batching concept (Step 4) with the immutable array update pattern.
5. (Spaced review — Step 1: Declarative vs Imperative)
A counter component needs to display the count and update when clicked. A student proposes three approaches. Which is correct?
A: document.getElementById('count').textContent = newCount
B: const [count, setCount] = useState(0); return <p>{count}</p>;
C: let count = 0; return <p>{count}</p>; with count++ on click
This question combines three concepts: (A) direct DOM manipulation bypasses React’s
declarative model (Step 1); (C) plain variables reset on every render and don’t trigger
re-renders (Step 4); (B) useState is the correct pattern — it persists across renders
and triggers re-rendering. The student must discriminate between declarative vs. imperative
(Step 1) AND state vs. plain variables (Step 4).
Learning objective: After this step you will be able to use the
childrenprop for composition, and apply the “Thinking in React” methodology to decompose a UI.
This step asks you to combine everything you have learned into a structured design process. It is normal to feel overwhelmed by the number of moving parts — components, props, state, lists, conditionals. Take it one step at a time: start with a static version (no state), then add interactivity piece by piece.
React’s official methodology for approaching a new UI:
In C++ and Java, you used inheritance (class Dog : Animal) to reuse code. React uses composition — you build complex UIs by combining small, generic components:
// SUB-GOAL: Define a generic container component
function Card({ children, className }) {
return <div className={'card ' + (className || '')}>{children}</div>;
}
// SUB-GOAL: Compose specific UI by nesting inside the container
function ProfileCard({ user }) {
return (
<Card className="profile">
<Avatar src={user.avatar} />
<h3>{user.name}</h3>
</Card>
);
}
The children prop lets any content be nested inside a component, making it a composable container — analogous to C++ templates or Python’s *args.
When two sibling components need the same data, move the state to their lowest common ancestor and pass it down as props. The child notifies the parent via a callback prop:
function Parent() {
const [text, setText] = React.useState('');
return (
<>
<SearchBar value={text} onChange={setText} />
<ResultsList filter={text} />
</>
);
}
As your component tree grows, you may find yourself passing a prop through several intermediate components that don’t use it — just so a deeply nested child can access it. This is called prop drilling:
App → Profile → Sidebar → UserCard (only UserCard uses the `user` prop)
Prop drilling is not a bug, but it makes code harder to maintain. If you are drilling more than 2-3 levels, consider React’s Context API (not covered in this tutorial) to share data without threading it through every layer.
Before writing any code, look at the user data in App. Predict: how many components do you need? Which component should accept children? Which should receive individual props like label and value? Sketch a component tree on paper (or in your head), then compare with the specification below.
Implement the component structure below. The specification is intentionally open-ended — there is no “correct” visual design.
Specification:
Avatar: Renders a circular image (use the provided avatarUrl) and the user’s usernameStatBadge: Shows a label and a value side by side (e.g. “Repos 42”)ProfileCard: Uses Avatar and three StatBadge components to build the full cardApp: Renders two ProfileCard components with the provided user dataChallenge (optional): After passing the tests, add a third user to the users array in App. Does your component hierarchy display the new card without any changes to Avatar, StatBadge, or ProfileCard? If yes, your composition is working — the same components render any number of users.
// Task: Implement Avatar
// Props: avatarUrl (string), username (string)
// Should render a circular image and the username text
function Avatar({ avatarUrl, username }) {
return (
<div>
{/* Your implementation */}
</div>
);
}
// Task: Implement StatBadge
// Props: label (string), value (number)
// Should show the label and value — e.g. "Repos 42"
function StatBadge({ label, value }) {
return (
<div>
{/* Your implementation */}
</div>
);
}
const { Card } = ReactBootstrap;
// Task: Implement ProfileCard using Avatar and StatBadge
// Props: user object with: name, username, avatarUrl, repos, followers, following
function ProfileCard({ user }) {
return (
<Card className="shadow-sm profile-card">
<Card.Body>
{/* Task: Use Avatar and StatBadge here */}
</Card.Body>
</Card>
);
}
function App() {
const users = [
{
name: 'Ada Lovelace',
username: 'ada-lovelace',
avatarUrl: 'https://i.pravatar.cc/80?img=47',
repos: 12, followers: 2048, following: 64
},
{
name: 'Alan Turing',
username: 'alan-turing',
avatarUrl: 'https://i.pravatar.cc/80?img=60',
repos: 7, followers: 9999, following: 3
},
];
return (
<div className="p-4 d-flex gap-4 flex-wrap bg-light min-vh-100">
{users.map(user => (
<ProfileCard key={user.username} user={user} />
))}
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
1. React favors composition over inheritance. Which statement best explains why?
Deep inheritance chains make it hard to understand or change one level without breaking another. React’s component model encourages building a Dialog from a generic Card, passing specific content as children — rather than creating a DialogCard extends Card hierarchy.
2. What does the children prop give you?
children is an implicit prop containing whatever JSX is placed between <MyComponent> and </MyComponent>. This is the foundation of composable container components.
3. A <SearchBar> and <ProductTable> are siblings. The user types in SearchBar and the table should filter. Where should filterText state live?
Lifting state up: state belongs in the lowest common ancestor of all components that need it. SearchBar receives filterText as a prop and calls onFilterChange(e.target.value) on input. The parent updates state, triggering a re-render of both.
4. A <UserCard> needs a user prop from a grandparent, passing through <Profile> which doesn’t use it. What is this antipattern called?
Prop drilling occurs when you pass props through layers of components that don’t use them. Solutions: React Context API (for widely-shared state) or state management libraries. Rule of thumb: if drilling more than 2-3 levels, reconsider.
5. (Spaced review — Step 5: Lists & Keys)
A drag-and-drop todo list lets users reorder items. Each item has a text input for editing. The current code uses key={index}. A user drags item C from position 3 to position 1. What happens to the text typed into item A’s input field?
With index-based keys, React identifies components by position. After reordering,
position 0 is now item C, but React thinks it is still “the same component” (key=0) —
so it keeps item A’s old input state and pairs it with item C’s text. This is why stable
IDs (key={task.id}) are essential for dynamic lists. This tests the consequence
of bad keys in a realistic scenario, not just the rule.
Learning objective: After this step you will be able to design and implement a complete React application that integrates components, props, state, lists, conditional rendering, and composition.
In Steps 1-7 you had scaffolding: pre-built component signatures, provided data structures, and step-by-step task lists. This step has none of that. You decide the component hierarchy, where state lives, and how data flows. This is intentional — applying concepts independently is how they transfer from tutorial exercises to real projects.
If you feel uncertain, that is a sign the earlier steps did their job: you know enough to recognize the design space, but haven’t yet internalized the patterns through unguided practice. Work through the “Thinking in React” methodology below — it will guide your decisions.
Build a mini product store with the following features:
.map() with proper key propsonSale is trueuseState to track cart itemsuseState for the active filterProductCard, CartSummary, FilterBar)Before coding, plan your component hierarchy:
const [cart, setCart] = React.useState([])setCart([...cart, product])cart.reduce((sum, item) => sum + item.price, 0).toFixed(2)Real-world data is messy. What if a product’s price is undefined or a string? You can guard against this with default values and optional chaining:
// Default value — if price is missing, show 0.00
<p>${(price ?? 0).toFixed(2)}</p>
// Optional chaining — safely access nested properties
<p>{product?.category}</p>
You do not need these for the tests (the data is clean), but they are essential habits for production code.
// Integration Project: Build a mini product store.
// No scaffolding — apply everything you have learned.
// Available: ReactBootstrap.Card, .Button, .Badge, .ButtonGroup, .ListGroup, etc.
const { Card, Button, Badge, ButtonGroup } = ReactBootstrap;
const products = [
{ id: 1, name: 'Wireless Mouse', price: 29.99, category: 'Electronics', onSale: false },
{ id: 2, name: 'Mechanical Keyboard', price: 89.99, category: 'Electronics', onSale: true },
{ id: 3, name: 'USB-C Hub', price: 45.99, category: 'Electronics', onSale: false },
{ id: 4, name: 'Laptop Stand', price: 34.99, category: 'Accessories', onSale: true },
{ id: 5, name: 'Desk Mat', price: 19.99, category: 'Accessories', onSale: false },
{ id: 6, name: 'Cable Management Kit', price: 14.99, category: 'Accessories', onSale: false },
];
// Build your components and App here
function App() {
return (
<div className="p-4">
<h1 className="h2 mb-4">Mini Store</h1>
{/* Your implementation */}
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
1. Evaluate this code for a mini store. What are the bugs?
function App() {
let cart = [];
const addToCart = (product) => { cart.push(product); };
return (
<div>
{products.map(p => <ProductCard product={p} onAdd={addToCart} />)}
<p>Cart: {cart.length} items</p>
</div>
);
}
(1) let cart = [] resets on every render and .push() mutates in-place without triggering a re-render. Fix: const [cart, setCart] = React.useState([]) and setCart([...cart, product]).
(2) Each mapped element needs a key prop: <ProductCard key={p.id} .../>.
2. Analyze the component design of a store app. A student puts product rendering, cart management, filtering, and the total calculation ALL in the App component. What is wrong with this approach?
Single-responsibility applies to components just as it does to C++ classes. A ProductCard
can be tested and reused independently. A CartSummary can be modified without touching
product display logic. This is Step 1 of “Thinking in React”: decompose the UI into a
component hierarchy where each component does one job.
3. In the mini store, both ProductCard (to show “In Cart” status) and CartSummary (to show the total) need access to the cart array. Where should cart state live?
Lifting state up: the cart state belongs in the lowest common ancestor of all components
that need it. App owns [cart, setCart], passes cart to CartSummary and an
onAdd callback to ProductCard. This is the same “Thinking in React” pattern from Step 7.
4. (Spaced review — Step 6: Conditional Rendering) A product card should show “In Cart” only when the product is already in the cart array. Which JSX pattern is correct?
&& short-circuit is the correct pattern for show/hide. Use .filter() with item.id === product.id
to check membership by ID rather than object reference (reference equality on objects is unreliable
after state updates create new arrays — [...cart, product] creates a new array, so cart.includes(product) may fail).
Option A is invalid syntax — ternary requires both branches (? ... : ...). Option C is invalid — if statements
cannot appear inside JSX expression braces (JSX only accepts expressions, not statements).