What are Components?
Components are independent, reusable pieces of UI. Imagine building a house: instead of crafting everything from scratch, you use pre-made components like doors, windows, and walls. Similarly, in React, you build UIs by combining components.
Types of Components
Function Components (Modern Approach)
Function components are like recipes - they take ingredients (props) and return a dish (UI):
// Simple function component
function Welcome(props) {
return <h1>Hello, {props.name}!</h1>;
}
// Arrow function component
const Welcome = (props) => {
return <h1>Hello, {props.name}!</h1>;
};
// Even simpler with implicit return
const Welcome = (props) => <h1>Hello, {props.name}!</h1>;
// Using the component
<Welcome name="Sarah" />
Class Components (Legacy Approach)
Class components are like kitchen appliances - more complex but with additional features:
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}!</h1>;
}
}
// Class components have additional features like:
// - Lifecycle methods
// - this binding
// - State management (before hooks)
Understanding Props
Props (short for properties) are how components talk to each other. Think of props as parameters passed to a function:
Basic Props Usage
// Passing props to a component
function App() {
return (
<div>
<UserCard
name="John Doe"
email="john@example.com"
age={30}
isActive={true}
/>
</div>
);
}
// Receiving props in a component
function UserCard(props) {
return (
<div className="user-card">
<h2>{props.name}</h2>
<p>Email: {props.email}</p>
<p>Age: {props.age}</p>
<p>Status: {props.isActive ? 'Active' : 'Inactive'}</p>
</div>
);
}
Props Destructuring
Destructuring makes props cleaner and more readable:
// Without destructuring
function UserCard(props) {
return (
<div>
<h2>{props.name}</h2>
<p>{props.email}</p>
</div>
);
}
// With destructuring
function UserCard({ name, email }) {
return (
<div>
<h2>{name}</h2>
<p>{email}</p>
</div>
);
}
// Destructuring with default values
function Button({
text = 'Click me',
color = 'blue',
size = 'medium'
}) {
return (
<button className={`btn btn-${color} btn-${size}`}>
{text}
</button>
);
}
Props Are Read-Only
Props are immutable - like ingredients that can't be changed once passed to a recipe:
// ❌ WRONG - Never modify props
function Wrong(props) {
props.name = 'New Name'; // This will cause an error!
return <h1>{props.name}</h1>;
}
// ✅ CORRECT - Props are read-only
function Correct(props) {
const displayName = props.name.toUpperCase();
return <h1>{displayName}</h1>;
}
Children Props
The special "children" prop allows components to be nested like Russian dolls:
// Component that uses children
function Card({ children, title }) {
return (
<div className="card">
<div className="card-header">
<h3>{title}</h3>
</div>
<div className="card-body">
{children}
</div>
</div>
);
}
// Using the Card component
function App() {
return (
<Card title="User Profile">
<p>Name: John Doe</p>
<p>Email: john@example.com</p>
<button>Edit Profile</button>
</Card>
);
}
Passing Different Types of Props
1. String Props
<Greeting message="Hello World" />
<Greeting message={'Hello World'} /> {/* Same as above */}
2. Number Props
<Counter start={0} />
<Counter start={42} />
3. Boolean Props
<Button disabled={true} />
<Button disabled /> {/* Shorthand for true */}
<Button disabled={false} />
4. Array Props
<List items={['Apple', 'Banana', 'Orange']} />
<List items={[1, 2, 3, 4, 5]} />
5. Object Props
<UserProfile
user={{
name: 'John',
age: 30,
email: 'john@example.com'
}}
/>
6. Function Props (Callbacks)
<Button onClick={() => alert('Clicked!')} />
<Button onClick={handleClick} />
Component Composition vs Inheritance
React favors composition over inheritance. It's like cooking: instead of creating complex inheritance hierarchies, you combine simple ingredients:
// Composition example
function WelcomeDialog() {
return (
<Dialog
title="Welcome"
message="Thank you for visiting our site!"
/>
);
}
function Dialog({ title, message }) {
return (
<div className="dialog">
<h1>{title}</h1>
<p>{message}</p>
</div>
);
}
// More complex composition
function SignUpDialog() {
return (
<Dialog title="Sign Up">
<input type="email" placeholder="Email" />
<input type="password" placeholder="Password" />
<button>Sign Up</button>
</Dialog>
);
}
Props Best Practices
1. Use PropTypes for Type Checking
import PropTypes from 'prop-types';
function UserCard({ name, age, email }) {
return (
<div>
<h2>{name}</h2>
<p>Age: {age}</p>
<p>Email: {email}</p>
</div>
);
}
UserCard.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
email: PropTypes.string.isRequired
};
2. Use Default Props
function Button({ text, color, size }) {
return (
<button className={`btn btn-${color} btn-${size}`}>
{text}
</button>
);
}
Button.defaultProps = {
text: 'Click me',
color: 'blue',
size: 'medium'
};
Component Patterns
1. Container and Presentational Components
// Container Component (handles logic)
function UserListContainer() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetchUsers().then(setUsers);
}, []);
return <UserList users={users} />;
}
// Presentational Component (handles UI)
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
2. Compound Components
// Compound components work together
function Tabs({ children }) {
const [activeTab, setActiveTab] = useState(0);
return (
<div className="tabs">
{React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
isActive: index === activeTab,
onActivate: () => setActiveTab(index)
});
})}
</div>
);
}
function Tab({ isActive, onActivate, children }) {
return (
<div
className={`tab ${isActive ? 'active' : ''}`}
onClick={onActivate}
>
{children}
</div>
);
}
// Usage
<Tabs>
<Tab>Home</Tab>
<Tab>About</Tab>
<Tab>Contact</Tab>
</Tabs>
Real-World Component Examples
1. Product Card Component
function ProductCard({ product, onAddToCart }) {
const { name, price, image, description, inStock } = product;
return (
<div className="product-card">
<img src={image} alt={name} />
<h3>{name}</h3>
<p>{description}</p>
<div className="price">${price}</div>
<button
onClick={() => onAddToCart(product)}
disabled={!inStock}
>
{inStock ? 'Add to Cart' : 'Out of Stock'}
</button>
</div>
);
}
2. Navigation Component
function Navigation({ links, currentPath }) {
return (
<nav>
<ul>
{links.map(link => (
<li key={link.path}>
<a
href={link.path}
className={currentPath === link.path ? 'active' : ''}
>
{link.label}
</a>
</li>
))}
</ul>
</nav>
);
}
// Usage
<Navigation
links={[
{ path: '/', label: 'Home' },
{ path: '/about', label: 'About' },
{ path: '/contact', label: 'Contact' }
]}
currentPath="/"
/>
Component Communication Patterns
// Parent managing shared state
function ShoppingCart() {
const [items, setItems] = useState([]);
const addItem = (item) => {
setItems([...items, item]);
};
const removeItem = (itemId) => {
setItems(items.filter(item => item.id !== itemId));
};
return (
<div>
<ProductList onAddToCart={addItem} />
<CartSummary
items={items}
onRemoveItem={removeItem}
/>
</div>
);
}
Component Lifecycle and Props
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch new user data when userId changes
setLoading(true);
fetchUser(userId)
.then(userData => {
setUser(userData);
setLoading(false);
});
}, [userId]); // Re-run effect when userId changes
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}