Week 10 Session 1: Props, State & Interactive Components
Introduction
In Week 8, you built your first React components – ProfileCard, Button, Header – and composed them into a complete app. But every ProfileCard showed the same person and every Button said the same text. The components were usable but not flexible. And nothing was interactive – clicking a button did nothing visible.
Today we fix both problems. Props make components reusable by passing different data to each instance. State makes components interactive by letting them manage data that changes. Together with callback props, these form the core communication pattern in React: data flows down, actions flow up.
What You’ll Learn
- What props are and how they relate to function arguments
- How to pass and destructure props
- Default values for safety
- The children prop for wrapper components
useStatefor managing changeable data- Callback props for child-to-parent communication
- The “data down, actions up” pattern
Part 1: Props Fundamentals
The Week 8 Problem
function ProfileCard() {
const name = "Jane Smith"; // hardcoded!
const role = "Web Developer";
return (
<div className="profile-card">
<h2>{name}</h2>
<p>{role}</p>
</div>
);
}
// Three identical cards
<ProfileCard />
<ProfileCard />
<ProfileCard />The Props Solution
function ProfileCard(props) {
return (
<div className="profile-card">
<h2>{props.name}</h2>
<p>{props.role}</p>
</div>
);
}
// Three DIFFERENT cards
<ProfileCard name="Jane Smith" role="Developer" />
<ProfileCard name="Bob Jones" role="Designer" />
<ProfileCard name="Alice Chen" role="Student" />Destructuring Props
// Standard React pattern -- destructure in the parameter
function ProfileCard({ name, role, bio }) {
return (
<div className="profile-card">
<h2>{name}</h2>
<p>{role}</p>
<p>{bio}</p>
</div>
);
}Default Values
function ProfileCard({
name,
role = "Team Member",
bio = "No bio provided"
}) {
return (
<div>
<h2>{name}</h2>
<p>{role}</p>
<p>{bio}</p>
</div>
);
}
<ProfileCard name="Jane" />
// role = "Team Member", bio = "No bio provided"Data Types as Props
| Type | Syntax | Notes |
|---|---|---|
| String | name="Jane" | Quotes only |
| Number | age={25} | Curly braces |
| Boolean | isActive={true} | Curly braces |
| Array | skills={["React", "CSS"]} | Curly braces |
| Object | address={{ city: "Chicago" }} | Double curly braces |
Props + .map() = Dynamic Lists
const teamMembers = [
{ id: 1, name: "Jane Smith", role: "Developer" },
{ id: 2, name: "Bob Jones", role: "Designer" },
{ id: 3, name: "Alice Chen", role: "Student" },
];
function App() {
return (
<div>
{teamMembers.map(member => (
<ProfileCard
key={member.id}
name={member.name}
role={member.role}
/>
))}
</div>
);
}One definition. Three different cards. Add a fourth person to the array and a fourth card appears automatically.
Part 2: Children Prop & Composition
The Children Prop
Regular props are attributes. Children is whatever goes between the tags:
<Card title="About Me">
<p>This content becomes children!</p>
</Card>
function Card({ title, children }) {
return (
<div className="card">
<h3>{title}</h3>
<div className="card-body">
{children}
</div>
</div>
);
}The same wrapper component can hold completely different content:
<Card title="Skills">
<ul>
<li>React</li>
<li>CSS</li>
</ul>
</Card>
<Card title="Quote">
<blockquote>
"Code is like humor. When you have to explain it, it's bad."
</blockquote>
</Card>Same Card component. One wraps a list, the other wraps a quote. Card doesn’t know or care what’s inside.
Wrapper Components
function Section({ title, children }) {
return (
<section>
<h2>{title}</h2>
<div>{children}</div>
</section>
);
}
function PageLayout({ children }) {
return (
<div className="page">
<Header />
<main>{children}</main>
<Footer />
</div>
);
}Build these once, reuse across your entire app.
Going deeper: Composition over Inheritance
React’s approach: build from pieces, not hierarchies. Instead of creating a FancyCard that extends Card, you compose:
// Composition -- ProfileCard uses Card
function ProfileCard({ name, role }) {
return (
<Card>
<h3>{name}</h3>
<p>{role}</p>
</Card>
);
}ProfileCard doesn’t inherit from Card. It just uses Card as a wrapper – passing its content as children. This is the React way.
Part 3: Managing State with useState
Why State?
Props are read-only – they come from the parent and can’t be changed. But real UIs need data that changes:
- A like count that goes up when clicked
- A form input that updates as you type
- A menu that opens and closes
- A list that filters based on a search
State is data that a component owns and can change. When state changes, React automatically re-renders the component.
The useState Hook
import { useState } from 'react'; at the top of any file that uses useState. Missing this import is one of the most common errors.import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
+1
</button>
</div>
);
}How useState works:
useState(0)creates a state variable with initial value0- It returns an array:
[currentValue, updaterFunction] - We destructure it:
const [count, setCount] = useState(0) - Calling
setCount(newValue)updates the value AND re-renders the component
Breaking Down the Syntax
const [count, setCount] = useState(0);
// ^ ^ ^
// current updater initial
// value function valueThis is array destructuring – the same concept as object destructuring but with position instead of name. useState returns an array with two items, and we give them names.
What does array destructuring look like without the shorthand?
// Without destructuring
const stateArray = useState(0);
const count = stateArray[0]; // current value
const setCount = stateArray[1]; // updater function
// With destructuring (same thing, cleaner)
const [count, setCount] = useState(0);You can name the variables anything, but the convention is [thing, setThing].
Why const if the value changes?
const [count, setCount] = useState(0) uses const, but the value seems to change when we call setCount. Here’s what’s actually happening:
Within a single render, count never changes – it’s truly constant. When you call setCount(1), React doesn’t change count from 0 to 1. Instead, React re-renders the component – it calls your function again from scratch. This time, useState(0) returns 1 (the updated value) instead of 0. It’s a brand new const variable with the new value.
Think of it like this: each render is a snapshot. In render #1, count = 0. In render #2, count = 1. Each snapshot has its own constant value.
What Happens When You Call the Setter
// User clicks the button
setCount(count + 1);
// React does two things:
// 1. Updates the value: count is now 1
// 2. Re-renders the component with the new valueThe component function runs again. This time useState(0) returns 1 (the updated value) instead of 0. The JSX renders with the new count.
useState is smart – instead of returning the initial value, it returns the updated value. This is how the UI stays in sync with your data.Multiple State Variables
Each component can have as many state variables as it needs:
function ProfileEditor() {
const [name, setName] = useState("");
const [bio, setBio] = useState("");
const [likes, setLikes] = useState(0);
return (
<div>
<p>{name} - {likes} likes</p>
<button onClick={() => setLikes(likes + 1)}>
Like
</button>
</div>
);
}Each state variable is completely independent – updating one doesn’t affect the others.
Rules of Hooks
Two rules you must follow:
- Only call
useStateat the top level of your component – not insideifstatements, loops, or nested functions - Only call
useStateinside React components (or custom hooks)
These rules ensure React can correctly track your state between re-renders.
// NEVER do this
function Counter() {
if (someCondition) {
const [count, setCount] = useState(0); // WRONG
}
// ...
}
// ALWAYS do this
function Counter() {
const [count, setCount] = useState(0); // top level
// ...
}Props vs State
| Props | State | |
|---|---|---|
| Who owns it? | Parent component | This component |
| Can it change? | No (read-only) | Yes (via setter function) |
| How is it set? | By the parent as attributes | By useState inside the component |
| When it changes | Parent re-renders child | Component re-renders itself |
Part 4: Callback Props – Data Down, Actions Up
The Communication Problem
Data flows DOWN via props. But what happens when a child component needs to notify the parent?
For example: a LikeButton inside a ProfileCard needs to tell the App component “the user clicked like!” so the App can update a total count.
The Solution: Pass a Function as a Prop
function App() {
const [totalLikes, setTotalLikes] = useState(0);
function handleLike() {
setTotalLikes(totalLikes + 1);
}
return (
<div>
<p>Total Likes: {totalLikes}</p>
<LikeButton onLike={handleLike} />
</div>
);
}
function LikeButton({ onLike }) {
return (
<button onClick={onLike}>
Like
</button>
);
}App creates state
const [totalLikes, setTotalLikes] = useState(0) – App owns the data.
App creates a handler function
handleLike calls setTotalLikes to update the state.
App passes the function as a prop
<LikeButton onLike={handleLike} /> – the function goes DOWN as a prop.
Child calls the function
When the button is clicked, onLike() runs – which is actually handleLike from App.
State updates and React re-renders
setTotalLikes updates the count. React re-renders App with the new value.
The Pattern: Data Down, Actions Up
App (owns state)
/ \
data ↓ ↓ function (as prop)
\ /
LikeButton
↑
calls onLike()| Direction | What Flows | How |
|---|---|---|
| Down | Data (values) | Parent passes props |
| Up | Actions (events) | Child calls parent’s function |
Why Does the Parent Own the State?
If the LikeButton owned the like count, the parent (App) couldn’t see it – data flows DOWN, not up. By keeping state in the parent, the parent can:
- Display the total count
- Pass the count to other components
- Decide how to update it
What’s the naming convention for callback props?
The convention is onSomething for the prop name and handleSomething for the function:
// In the parent
function handleLike() { ... }
function handleDelete() { ... }
function handleSearch(query) { ... }
// Passing to the child
<Button onLike={handleLike} />
<Item onDelete={handleDelete} />
<SearchBar onSearch={handleSearch} />The on prefix signals “this is a callback.” The handle prefix signals “this handles the event.”
Conditional Rendering with &&
In the examples above (and in many React apps), you’ll see a pattern like {condition && <element>}. This is conditional rendering – showing or hiding parts of the UI based on a condition.
// Only show the button if onFavorite was passed as a prop
{onFavorite && (
<button onClick={() => onFavorite(name)}>
Favorite
</button>
)}
// Only show the list if there are favorites
{favorites.length > 0 && (
<p>Favorites: {favorites.join(', ')}</p>
)}
// Only show the title if one was provided
{title && <h3>{title}</h3>}How it works: In JavaScript, && returns the second value if the first is truthy, or the first value if it’s falsy. JSX ignores false, null, and undefined, so when the condition is false, nothing renders.
{condition && <JSX>} everywhere in React code. It’s the simplest way to conditionally show or hide elements without needing a full if statement.Troubleshooting
Props showing as undefined
Children prop is undefined
<Card>content</Card> not <Card />.I’m getting ‘props is not defined’
If you’re using destructuring, you don’t have a props variable:
// Can't use props.name with destructuring
function ProfileCard({ name }) {
return <h2>{props.name}</h2>; // Error!
}
// Use the destructured variable directly
function ProfileCard({ name }) {
return <h2>{name}</h2>; // Correct!
}State not updating when I click
Common causes:
- Forgot
onClick: Make sure the button hasonClick={() => setCount(count + 1)} - Calling the function immediately: Use
onClick={() => setCount(count + 1)}notonClick={setCount(count + 1)}– the arrow function prevents immediate execution - Mutating state directly: Don’t do
count++. UsesetCount(count + 1).
// WRONG -- calls setCount immediately on render
<button onClick={setCount(count + 1)}>
// RIGHT -- calls setCount when clicked
<button onClick={() => setCount(count + 1)}>Callback prop not working
Make sure:
- The parent is passing the function:
<Child onAction={handleAction} /> - The child is calling it:
onClick={onAction}oronClick={() => onAction(data)} - The function names match between parent and child
All cards update when I click one
useState call. If you have a likes state in the parent and pass it to all cards, they’ll share it. Instead, put useState inside each card component so each has independent state.Getting ‘React Hook useState is called conditionally’
You have a useState call inside an if statement, loop, or after a return. Move it to the top of your component, before any conditions.
// WRONG
function MyComponent({ show }) {
if (!show) return null;
const [count, setCount] = useState(0); // Error!
}
// RIGHT
function MyComponent({ show }) {
const [count, setCount] = useState(0); // Top level
if (!show) return null;
// ... rest of component
}Putting It All Together: Interactive Card System
Here’s a complete example that connects every concept – props, children, useState, callback props, conditional rendering, and .map() – in a realistic multi-component app.
The Component Tree
App (favorites state)
├── CardGrid (columns=4) ← wrapper using children
│ ├── InfoCard → Card (Team Members: 3)
│ └── InfoCard → Card (Favorites: 0 → updates!)
├── CardGrid (columns=3) ← wrapper using children
│ ├── ProfileCard (likes state) → Card
│ ├── ProfileCard (likes state) → Card
│ └── ProfileCard (likes state) → Card
└── Favorites list (conditional rendering)ProfileCard: Local State + Callback Props
This component demonstrates local state (likes), composition (wraps content in <Card>), callback props (onFavorite), and conditional rendering with &&:
import { useState } from 'react';
import Card from './Card';
function ProfileCard({ name, role = "Team Member", bio = "No bio provided", onFavorite }) {
const [likes, setLikes] = useState(0);
return (
<Card>
<h3>{name}</h3>
<p>{role}</p>
<p>{bio}</p>
<div>
<button onClick={() => setLikes(likes + 1)}>
{likes} Likes
</button>
{onFavorite && (
<button onClick={() => onFavorite(name)}>
Favorite
</button>
)}
</div>
</Card>
);
}Two types of interaction here:
- Likes use local state – each ProfileCard tracks its own count independently. Clicking Like on Jane’s card doesn’t affect Bob’s card.
- Favorite uses a callback prop – ProfileCard doesn’t know what happens when you click it. It just calls
onFavorite(name)and lets the parent decide.
App: Lifted State + Callbacks + .map()
The App component owns the favorites state and passes a callback down to each ProfileCard:
import { useState } from 'react';
import ProfileCard from './ProfileCard';
import CardGrid from './CardGrid';
const teamMembers = [
{ id: 1, name: "Jane Smith", role: "Lead Developer", bio: "Loves building with React" },
{ id: 2, name: "Bob Jones", role: "UX Designer", bio: "Makes everything look great" },
{ id: 3, name: "Alice Chen", role: "Full Stack Dev", bio: "JavaScript enthusiast" },
];
function App() {
const [favorites, setFavorites] = useState([]);
function handleFavorite(name) {
if (favorites.includes(name)) {
setFavorites(favorites.filter(fav => fav !== name));
} else {
setFavorites([...favorites, name]);
}
}
return (
<div>
<h1>Dashboard</h1>
<CardGrid columns={3}>
{teamMembers.map(member => (
<ProfileCard
key={member.id}
name={member.name}
role={member.role}
bio={member.bio}
onFavorite={handleFavorite}
/>
))}
</CardGrid>
{favorites.length > 0 && (
<p>Favorites: {favorites.join(', ')}</p>
)}
</div>
);
}Data Flow
App
├── favorites (state) ─────────────────────────┐
├── handleFavorite (function) ──────────┐ │
│ │ │
├── InfoCard ← value={favorites.length} ┘ │
│ │
└── ProfileCard ← onFavorite={handleFavorite} ──┘
├── likes (local state)
└── onClick → onFavorite(name) → App updates favoritesKey observations:
- Each ProfileCard has independent
likesstate (local state – only that card is affected) favoriteslives in App and flows down through props, back up through callbacks (lifted state)- Card and CardGrid are pure wrapper components using
children– they don’t know their content - This is the “data down, actions up” pattern in a real project
Key Takeaways
- Props = function arguments – pass data from parent to child
- Destructure for clean syntax:
{ name, role } - Defaults for safety:
role = "Team Member" - Props are read-only – display them, never modify them
- Children prop = flexible content between tags
- useState = data the component owns and can change (don’t forget
import { useState } from 'react') - Calling the setter re-renders the component – the entire function runs again with the new value
- Rules of hooks – call
useStateat the top level only, inside components only - Callback props = pass functions down, child calls them up
- Data down, actions up = the core React communication pattern
- Conditional rendering with
&&–{condition && <JSX>}shows/hides elements - Local vs lifted state – local state stays in one component; lifted state lives in a parent and flows via props/callbacks