Props: The Communication Channel
Props (short for "properties") are React's mechanism for passing data from parent to child components. They are the primary way components communicate with each other and form the backbone of React's unidirectional data flow pattern.
Analogy: Mail Delivery System
Think of props like a mail delivery system:
- Parent Component: The post office that sends packages
- Props: The packages themselves, containing information
- Child Components: Homes that receive packages
- Read-Only Nature: Like sealed packages, props shouldn't be modified by recipients
- Unidirectional Flow: Mail always flows from the post office to homes, not vice versa
Just as the postal service only delivers packages downward (from central distribution to individual homes), React's data flows down the component tree through props.
Passing Props to Components
Props can be passed to components in several ways, similar to HTML attributes:
Basic Props Passing
// Parent component passing props
function App() {
return (
<div>
<UserProfile
name="John Doe"
role="Developer"
isActive={true}
loginCount={42}
/>
</div>
);
}
// Child component receiving props
function UserProfile(props) {
return (
<div className="user-profile">
<h2>{props.name}</h2>
<p>Role: {props.role}</p>
<p>Status: {props.isActive ? 'Active' : 'Inactive'}</p>
<p>Login count: {props.loginCount}</p>
</div>
);
}
Destructuring Props
// Destructuring in function parameter
function UserProfile({ name, role, isActive, loginCount }) {
return (
<div className="user-profile">
<h2>{name}</h2>
<p>Role: {role}</p>
<p>Status: {isActive ? 'Active' : 'Inactive'}</p>
<p>Login count: {loginCount}</p>
</div>
);
}
// Alternatively, destructure in function body
function UserProfile(props) {
const { name, role, isActive, loginCount } = props;
return (
<div className="user-profile">
<h2>{name}</h2>
<p>Role: {role}</p>
<p>Status: {isActive ? 'Active' : 'Inactive'}</p>
<p>Login count: {loginCount}</p>
</div>
);
}
Props with Default Values
// Using default parameters
function UserProfile({
name = 'Guest User',
role = 'Visitor',
isActive = false,
loginCount = 0
}) {
return (
<div className="user-profile">
<h2>{name}</h2>
<p>Role: {role}</p>
<p>Status: {isActive ? 'Active' : 'Inactive'}</p>
<p>Login count: {loginCount}</p>
</div>
);
}
// For class components or older method
UserProfile.defaultProps = {
name: 'Guest User',
role: 'Visitor',
isActive: false,
loginCount: 0
};
Best Practices for Props
- Keep component props focused - Pass only what's needed
- Use descriptive prop names - Self-documenting code is easier to maintain
- Provide default values - Makes components more robust
- Destructure for readability - Especially with many props
- Consider using prop objects - For related data (e.g.,
user={userData})
Props Can Be Various Data Types
React props can be any valid JavaScript values:
Primitive Values
<Component
stringProp="Hello World"
numberProp={42}
booleanProp={true}
nullProp={null}
undefinedProp={undefined}
/>
Objects
const user = {
id: 1,
name: 'John Doe',
email: 'john@example.com'
};
<UserCard user={user} />
Arrays
const items = ['Apple', 'Banana', 'Cherry'];
<ShoppingList items={items} />
Functions
function handleClick() {
console.log('Button clicked!');
}
<Button onClick={handleClick} />
React Elements
const title = <h1>User Dashboard</h1>;
<Panel header={title}>
Content goes here
</Panel>
Using the Spread Operator with Props
const buttonProps = {
type: 'submit',
className: 'primary-button',
disabled: false,
onClick: handleSubmit
};
// Spread all properties as individual props
<Button {...buttonProps} />
// Equivalent to:
<Button
type="submit"
className="primary-button"
disabled={false}
onClick={handleSubmit}
/>
The Special 'children' Prop
The children prop is a special prop that contains any elements included between the opening and closing tags of a component:
function Card({ title, children }) {
return (
<div className="card">
<div className="card-header">
<h2>{title}</h2>
</div>
<div className="card-body">
{children}
</div>
</div>
);
}
// Usage
<Card title="Welcome">
<p>This is the card content.</p>
<button>Learn More</button>
</Card>
Prop Types and Type Checking
React provides a way to validate props using PropTypes, helping catch bugs and document the intended data types.
Using PropTypes
import PropTypes from 'prop-types';
function UserProfile({ name, age, isAdmin, friends }) {
return (
<div>
<h2>{name}</h2>
<p>Age: {age}</p>
{isAdmin && <p>Admin</p>}
<div>
<h3>Friends</h3>
<ul>
{friends.map(friend => (
<li key={friend.id}>{friend.name}</li>
))}
</ul>
</div>
</div>
);
}
UserProfile.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
isAdmin: PropTypes.bool,
friends: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired
})
)
};
UserProfile.defaultProps = {
age: 21,
isAdmin: false,
friends: []
};
Common PropTypes Validators
MyComponent.propTypes = {
// Basic types
optionalString: PropTypes.string,
optionalNumber: PropTypes.number,
optionalBool: PropTypes.bool,
optionalFunc: PropTypes.func,
optionalObject: PropTypes.object,
optionalArray: PropTypes.array,
optionalSymbol: PropTypes.symbol,
// Anything that can be rendered
optionalNode: PropTypes.node,
// A React element
optionalElement: PropTypes.element,
// An element type (e.g., MyComponent)
optionalElementType: PropTypes.elementType,
// Instance of a class
optionalInstance: PropTypes.instanceOf(MyClass),
// One of specific values
optionalEnum: PropTypes.oneOf(['News', 'Photos']),
// One of several types
optionalUnion: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]),
// Array of specific type
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),
// Object with values of specific type
optionalObjectOf: PropTypes.objectOf(PropTypes.number),
// Object with specific shape
optionalObjectWithShape: PropTypes.shape({
name: PropTypes.string,
age: PropTypes.number
}),
// Required versions
requiredString: PropTypes.string.isRequired,
// Custom validator
customProp: function(props, propName, componentName) {
if (!/matchme/.test(props[propName])) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
}
};
TypeScript: A More Robust Alternative
For larger applications, TypeScript provides a more comprehensive type-checking system:
interface UserProfileProps {
name: string;
age?: number;
isAdmin?: boolean;
friends?: Array<{
id: number;
name: string;
}>;
}
function UserProfile({ name, age = 21, isAdmin = false, friends = [] }: UserProfileProps) {
return (
<div>
<h2>{name}</h2>
<p>Age: {age}</p>
{isAdmin && <p>Admin</p>}
<div>
<h3>Friends</h3>
<ul>
{friends.map(friend => (
<li key={friend.id}>{friend.name}</li>
))}
</ul>
</div>
</div>
);
}
Unidirectional Data Flow
React enforces a unidirectional data flow: data passes down from parent to child component through props. This makes applications more predictable and easier to debug.
Core Principles of Unidirectional Data Flow
- Props are Read-Only: Components should never modify the props they receive
- Data Flows Down: Parent components pass data to children, not vice versa
- Local State Management: Components manage their own state independently
- State Lifting: When multiple components need the same state, it's "lifted" to their common ancestor
Example of Unidirectional Data Flow
function App() {
// State in the parent component
const [user, setUser] = useState({
name: 'John Doe',
isAdmin: false
});
// Function to update state
const toggleAdminStatus = () => {
setUser(prevUser => ({
...prevUser,
isAdmin: !prevUser.isAdmin
}));
};
return (
<div className="app">
{/* Pass data and callback function as props */}
<UserProfile
user={user}
onToggleAdmin={toggleAdminStatus}
/>
</div>
);
}
function UserProfile({ user, onToggleAdmin }) {
return (
<div className="user-profile">
<h2>{user.name}</h2>
<p>Status: {user.isAdmin ? 'Admin' : 'Regular User'}</p>
{/* Call the function received from props */}
<button onClick={onToggleAdmin}>
{user.isAdmin ? 'Revoke Admin' : 'Make Admin'}
</button>
{/* Pass only necessary data to child component */}
<UserPermissions isAdmin={user.isAdmin} />
</div>
);
}
function UserPermissions({ isAdmin }) {
return (
<div className="permissions">
<h3>Permissions</h3>
{isAdmin ? (
<ul>
<li>Can view content</li>
<li>Can edit content</li>
<li>Can manage users</li>
<li>Can access settings</li>
</ul>
) : (
<ul>
<li>Can view content</li>
</ul>
)}
</div>
);
}
Analogy: Water Flow in a Waterfall
Think of React's data flow like a waterfall:
- Water (data) always flows from higher to lower levels (parent to child)
- Water cannot naturally flow upward (children can't directly modify parent data)
- Each pool (component) can only affect pools below it, not above it
- To change the flow at the source, you need a pump system (callback functions passed as props)
This consistent downward flow makes the system predictable and easier to debug, just like it's easier to trace water contamination in a waterfall (it must be from above) than in a complex network where water flows in multiple directions.
Lifting State Up
"Lifting state up" is a pattern in React where state is moved to a common ancestor of components that need it. This maintains the unidirectional data flow while allowing multiple components to share and update the same data.
Example: Temperature Converter
Let's look at an example where two components need to share state:
import { useState } from 'react';
function TemperatureConverter() {
// State is lifted to the parent component
const [temperature, setTemperature] = useState('');
const [scale, setScale] = useState('c'); // 'c' for Celsius, 'f' for Fahrenheit
// Helper functions for converting temperatures
const toCelsius = (fahrenheit) => {
return (fahrenheit - 32) * 5 / 9;
};
const toFahrenheit = (celsius) => {
return (celsius * 9 / 5) + 32;
};
// Event handlers
const handleCelsiusChange = (value) => {
setTemperature(value);
setScale('c');
};
const handleFahrenheitChange = (value) => {
setTemperature(value);
setScale('f');
};
// Calculate both temperatures based on input and scale
const celsius = scale === 'f' ? toCelsius(parseFloat(temperature)) : parseFloat(temperature);
const fahrenheit = scale === 'c' ? toFahrenheit(parseFloat(temperature)) : parseFloat(temperature);
return (
<div>
<h2>Temperature Converter</h2>
{/* Child component for Celsius input */}
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={handleCelsiusChange}
/>
{/* Child component for Fahrenheit input */}
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={handleFahrenheitChange}
/>
{/* Display water state */}
{temperature !== '' && !isNaN(celsius) && (
<div className="result">
{celsius >= 100 ? 'The water would boil.' : 'The water would not boil.'}
</div>
)}
</div>
);
}
function TemperatureInput({ scale, temperature, onTemperatureChange }) {
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
};
const handleChange = (e) => {
// Pass the new value back to the parent
onTemperatureChange(e.target.value);
};
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input
value={isNaN(temperature) ? '' : temperature}
onChange={handleChange}
type="number"
/>
</fieldset>
);
}
When to Lift State Up
- When multiple components need to reflect the same changing data
- When you need to synchronize state between components
- When child components need to update data that affects sibling components
- When you want a single source of truth for a particular piece of data
Remember: Lift state only as high as necessary, but no higher!
Passing Data from Child to Parent
Although React's data flow is unidirectional (parent to child), you can create a two-way communication pattern by passing callback functions as props from parent to child.
Simple Example: Form Input
function ParentComponent() {
const [name, setName] = useState('');
// This callback will be passed to the child
const handleNameChange = (newName) => {
setName(newName);
};
return (
<div className="parent">
<h1>Hello, {name || 'Stranger'}!</h1>
{/* Pass the callback to the child */}
<ChildComponent onNameChange={handleNameChange} />
</div>
);
}
function ChildComponent({ onNameChange }) {
const handleInputChange = (e) => {
// Call the parent's callback with the new value
onNameChange(e.target.value);
};
return (
<div className="child">
<label>
Enter your name:
<input type="text" onChange={handleInputChange} />
</label>
</div>
);
}
More Complex Example: Todo List
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', completed: false },
{ id: 2, text: 'Build a project', completed: false }
]);
// Callback to add a new todo
const addTodo = (text) => {
const newTodo = {
id: Date.now(),
text,
completed: false
};
setTodos([...todos, newTodo]);
};
// Callback to toggle completion status
const toggleTodo = (id) => {
setTodos(
todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
// Callback to delete a todo
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div className="todo-app">
<h1>Todo List</h1>
{/* Child component to add todos */}
<AddTodoForm onAddTodo={addTodo} />
{/* Child component to display and manage todos */}
<TodoItems
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
</div>
);
}
function AddTodoForm({ onAddTodo }) {
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
onAddTodo(text);
setText('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a new todo"
/>
<button type="submit">Add</button>
</form>
);
}
function TodoItems({ todos, onToggle, onDelete }) {
return (
<ul className="todo-list">
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
);
}
Real-World Pattern: Container and Presentational Components
A common pattern in React applications is to separate components into:
- Container Components: Manage state, data fetching, and business logic
- Presentational Components: Focus on UI rendering, receive data via props, and communicate events up via callbacks
// Container component
function UserContainer() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUser() {
try {
setLoading(true);
const response = await fetch('/api/user');
const data = await response.json();
setUser(data);
setError(null);
} catch (err) {
setError('Failed to fetch user data');
setUser(null);
} finally {
setLoading(false);
}
}
fetchUser();
}, []);
const handleUpdateProfile = async (updatedData) => {
try {
setLoading(true);
const response = await fetch('/api/user', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updatedData)
});
const data = await response.json();
setUser(data);
} catch (err) {
setError('Failed to update profile');
} finally {
setLoading(false);
}
};
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user data found</div>;
// Pass data and callbacks to presentational component
return <UserProfile user={user} onUpdate={handleUpdateProfile} />;
}
// Presentational component
function UserProfile({ user, onUpdate }) {
const [formData, setFormData] = useState({
name: user.name,
email: user.email
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = (e) => {
e.preventDefault();
onUpdate(formData); // Call the container's callback
};
return (
<div className="user-profile">
<h2>{user.name}'s Profile</h2>
<form onSubmit={handleSubmit}>
<div>
<label>
Name:
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
/>
</label>
</div>
<div>
<label>
Email:
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</label>
</div>
<button type="submit">Update Profile</button>
</form>
</div>
);
}
Props vs. State
Understanding the difference between props and state is fundamental to mastering React's data flow.
| Props | State |
|---|---|
| Received from parent component | Defined and managed within the component |
| Read-only, shouldn't be modified | Can be updated with setState/useState |
| Re-rendered when parent re-renders | Re-rendered when state changes |
| Access via props in functional components or this.props in class components |
Access via state/setState hooks in functional components or this.state/this.setState in class components |
| Used to pass data down the component tree | Used for component's internal data management |
Using Props and State Together
function Counter({ initialCount = 0, step = 1 }) {
// initialCount and step are props
// count is state initialized from a prop
const [count, setCount] = useState(initialCount);
const increment = () => {
// Using the step prop with internal state
setCount(count + step);
};
const decrement = () => {
setCount(count - step);
};
const reset = () => {
// Resetting state back to the initialCount prop
setCount(initialCount);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={decrement}>- {step}</button>
<button onClick={increment}>+ {step}</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Analogy: Props vs. State
- Props are like function arguments - They're passed in from the outside and shouldn't be changed by the function itself.
- State is like local variables - Created, used, and modified within the function's scope.
Another way to think about it:
- Props: Like the specifications a client gives you for a project - they come from outside and you work with them as given
- State: Like your internal notes and drafts while working on the project - you create and modify them as needed during the work
Common Props Patterns
Render Props Pattern
The render props pattern involves passing a function as a prop that returns a React element. This allows the parent component to control what gets rendered while the child handles the logic.
function MouseTracker() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e) => {
setPosition({
x: e.clientX,
y: e.clientY
});
};
return (
<div onMouseMove={handleMouseMove} style={{ height: '300px', border: '1px solid #ccc' }}>
<h2>Move your mouse around!</h2>
{/* Render prop pattern */}
<MousePosition
render={(pos) => (
<p>Current position: ({pos.x}, {pos.y})</p>
)}
position={position}
/>
</div>
);
}
function MousePosition({ render, position }) {
return render(position);
}
Compound Components Pattern
Compound components are components that work together to form a complete UI. The parent component typically manages shared state and provides context for its children.
function Tabs({ children, defaultTab }) {
const [activeTab, setActiveTab] = useState(defaultTab);
// Clone children to inject the active prop
const tabs = React.Children.map(children, child => {
if (child.type === Tab) {
return React.cloneElement(child, {
active: child.props.id === activeTab,
onActivate: () => setActiveTab(child.props.id)
});
}
return child;
});
return <div className="tabs">{tabs}</div>;
}
function Tab({ id, title, children, active, onActivate }) {
return (
<div className={`tab ${active ? 'active' : ''}`}>
<div className="tab-header" onClick={onActivate}>
{title}
</div>
{active && <div className="tab-content">{children}</div>}
</div>
);
}
// Usage
function App() {
return (
<Tabs defaultTab="profile">
<Tab id="profile" title="Profile">
<p>This is the profile tab</p>
</Tab>
<Tab id="settings" title="Settings">
<p>This is the settings tab</p>
</Tab>
<Tab id="notifications" title="Notifications">
<p>This is the notifications tab</p>
</Tab>
</Tabs>
);
}
Props Spreading and Destructuring
For components that forward props to underlying DOM elements or other components, props spreading can be useful.
function Button({ className, children, ...rest }) {
// Combine custom class with default button class
const buttonClass = `button ${className || ''}`.trim();
// Spread the remaining props to the button element
return (
<button className={buttonClass} {...rest}>
{children}
</button>
);
}
// Usage
function App() {
return (
<div>
<Button
onClick={() => alert('Clicked!')}
disabled={false}
className="primary"
data-testid="submit-button"
>
Click Me
</Button>
</div>
);
}
Be Careful with Props Spreading
While convenient, props spreading can sometimes pass unnecessary or even harmful props. Consider using more explicit props when:
- You need better prop type validation
- You're working on a security-sensitive component
- You want to avoid accidental prop overrides
- You want to make the component API more explicit
Context API for Deep Prop Passing
Sometimes, props need to be passed through many levels of components, which can lead to "prop drilling". React's Context API offers a solution to this problem by allowing data to be passed directly to any component in the tree without explicit prop passing.
Using Context API
import React, { createContext, useContext, useState } from 'react';
// Create a context with default value
const UserContext = createContext({
user: null,
setUser: () => {}
});
// Provider component to wrap the app
function UserProvider({ children }) {
const [user, setUser] = useState(null);
// The value prop contains the data to be shared
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
// Custom hook for easy context usage
function useUser() {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
// App component with the provider
function App() {
return (
<UserProvider>
<div className="app">
<Header />
<MainContent />
<Footer />
</div>
</UserProvider>
);
}
// No need to pass user props through these components
function MainContent() {
return (
<main>
<h2>Main Content</h2>
<ArticleList />
</main>
);
}
function ArticleList() {
return (
<div>
<h3>Articles</h3>
<Article id="1" title="Context API Explained" />
<Article id="2" title="Props vs. Context" />
</div>
);
}
function Article({ id, title }) {
return (
<article>
<h4>{title}</h4>
<p>Article content...</p>
<CommentSection articleId={id} />
</article>
);
}
// Deep component accessing context directly
function CommentSection({ articleId }) {
// Access user data from context
const { user } = useUser();
return (
<div className="comments">
<h5>Comments</h5>
{user ? (
<div>
<p>Comment as {user.name}</p>
<textarea placeholder="Write a comment..." />
<button>Submit</button>
</div>
) : (
<p>Please log in to comment</p>
)}
</div>
);
}
// Component that modifies the context
function Header() {
const { user, setUser } = useUser();
const login = () => {
// Simulated login
setUser({ id: 1, name: 'John Doe' });
};
const logout = () => {
setUser(null);
};
return (
<header>
<h1>My App</h1>
<div>
{user ? (
<>
<span>Welcome, {user.name}!</span>
<button onClick={logout}>Logout</button>
</>
) : (
<button onClick={login}>Login</button>
)}
</div>
</header>
);
}
When to Use Context vs. Props
- Use Props When:
- Data is only needed by immediate children
- Component reusability is critical
- Component relationships are clear and shallow
- Use Context When:
- Data is needed by many components at different levels
- Passing props would create excessive "prop drilling"
- You need a global state for themes, user auth, etc.
Practice Activities
Activity 1: Props and Data Flow
Objective: Practice passing props and implementing unidirectional data flow.
Instructions:
- Create a
ShoppingCartcomponent that manages a list of items - Create a
CartItemcomponent that displays an item with quantity controls - Create an
AddItemFormcomponent to add new items to the cart - Implement callbacks to update quantities and add/remove items
- Add a
CartSummarycomponent to display the total price
Ensure all data flow follows React's unidirectional pattern.
Activity 2: Lifting State Up
Objective: Practice the "lifting state up" pattern.
Instructions:
- Create a
FilterableProductTablecomponent with:SearchBarcomponent with text input and "in stock only" checkboxProductTablecomponent displaying filtered products
- Lift the filter state (search text and "in stock only" flag) to the parent component
- Implement callbacks to update the filter state
- Filter products based on the state before passing them to
ProductTable
Activity 3: Context API
Objective: Practice using Context API to avoid prop drilling.
Instructions:
- Create a
ThemeContextwith light and dark theme options - Implement a
ThemeProvidercomponent with state for the current theme - Create a deep component tree with at least 3 levels of nesting
- Add a
ThemeTogglebutton component that uses the context to change themes - Apply theme styles to various components at different levels of the tree
Compare this implementation with how it would look using props.
Resources for Further Learning
Official Documentation:
- Passing Props to a Component
- Sharing State Between Components
- Passing Data Deeply with Context
- createContext API Reference
Additional Resources:
- Prop Drilling by Kent C. Dodds
- How to Lift State in React by Robin Wieruch
- React Patterns at patterns.dev
Summary
- Props are the mechanism for passing data from parent to child components in React.
- React's data flow is unidirectional, flowing down the component tree.
- Props can be primitives, objects, arrays, functions, or even React elements.
- Type checking with PropTypes or TypeScript helps catch bugs and document component interfaces.
- Lifting state up to a common ancestor allows sharing data between components.
- Child-to-parent communication is achieved by passing callback functions as props.
- Props are read-only, while state is internal and mutable within a component.
- The Context API provides a way to share data across the component tree without prop drilling.
- Common patterns like render props and compound components offer flexible ways to compose components.
In the next lecture, we'll explore component composition patterns and learn how to build more complex UIs by combining simpler components.