đ§Š Components and Props
Components are the heart and soul of React - they're the building blocks you'll use to construct everything from simple buttons to complex applications. Think of components like LEGO bricks: each piece is self-contained and reusable, but when you snap them together, you can build anything! In this lesson, we'll learn how to create components that talk to each other using props, and discover the patterns that make React applications maintainable and scalable. Let's build something amazing! đ
đ¯ Learning Objectives
By the end of this lesson, you will be able to:
- Create and compose React components effectively
- Define and use TypeScript interfaces for props
- Pass data between components using props
- Use the special children prop for component composition
- Set default values for optional props
- Destructure props for cleaner code
- Apply component design patterns and best practices
Estimated Time: 60-75 minutes
Project: Build a card-based layout system with reusable components
đ In This Lesson
đ¯ Understanding Components
Components are the fundamental building blocks of React applications. Let's understand what makes them so powerful!
đ Definition
Component: A self-contained, reusable piece of UI that encapsulates its structure (markup), appearance (styles), and behavior (logic). Components accept inputs called "props" and return React elements describing what should appear on screen.
The Component Philosophy
React encourages you to break your UI into small, focused components. Each component should:
- Do one thing well - Single responsibility principle
- Be reusable - Work in different contexts with different data
- Be composable - Combine with other components to build complex UIs
- Be predictable - Same props = same output
đĄ Real-World Analogy
Think of a car. It's made of components: engine, wheels, seats, steering wheel. Each component has a specific job, can be replaced or upgraded independently, and they all work together to make the car function. That's exactly how React components work!
Component Hierarchy
React applications are organized as a tree of components:
This hierarchy shows:
- Parent components contain child components
- Data flows down from parents to children via props
- Reusability - NavLink and Article are used multiple times
- Organization - Clear structure makes code maintainable
Types of Components
Components can be categorized by their role:
| Type | Purpose | Example |
|---|---|---|
| Presentational | How things look | Button, Card, Modal |
| Container | How things work | UserListContainer, DataFetcher |
| Layout | Page structure | Page, Sidebar, Grid |
| Utility | Provide functionality | ErrorBoundary, ThemeProvider |
â Component Benefits
- Reusability - Write once, use many times
- Maintainability - Easy to update and fix bugs
- Testability - Test components in isolation
- Readability - Self-documenting code structure
- Collaboration - Team members work on different components
- Performance - React only re-renders what changed
đ ī¸ Creating Components
Let's learn the mechanics of creating React components with TypeScript!
Function Components (Modern React)
Function components are the standard way to create components in modern React:
// Basic function component
function Welcome() {
return <h1>Hello, World!</h1>;
}
// Arrow function component
const Goodbye = () => {
return <h1>Goodbye, World!</h1>;
};
// With TypeScript type annotation
const Greeting: React.FC = () => {
return <h1>Greetings!</h1>;
};
â ī¸ React.FC vs Plain Functions
You'll see React.FC (Function Component) in older code, but modern React recommends plain functions. Both work, but plain functions are simpler and more flexible.
// Modern (Recommended)
function MyComponent() {
return <div>Hello</div>;
}
// Older style (Still valid)
const MyComponent: React.FC = () => {
return <div>Hello</div>;
};
Component File Structure
Each component typically lives in its own file:
// src/components/Button.tsx
// 1. Imports
import { useState } from 'react';
import './Button.css';
// 2. Type definitions
interface ButtonProps {
text: string;
onClick: () => void;
}
// 3. Component definition
function Button({ text, onClick }: ButtonProps) {
const [isPressed, setIsPressed] = useState(false);
const handleClick = () => {
setIsPressed(true);
onClick();
setTimeout(() => setIsPressed(false), 200);
};
return (
<button
className={`btn ${isPressed ? 'pressed' : ''}`}
onClick={handleClick}
>
{text}
</button>
);
}
// 4. Export
export default Button;
Naming Conventions
Follow these conventions for clean, professional code:
- PascalCase for components -
UserProfile, notuserProfile - Descriptive names -
LoginButtonvsButton1 - File name matches component -
Button.tsxcontainsButton - One component per file - Exceptions for small, related helpers
// â
Good naming
function UserProfileCard() { /* ... */ }
function LoadingSpinner() { /* ... */ }
function SubmitButton() { /* ... */ }
// â Bad naming
function card() { /* ... */ } // Not PascalCase
function Component1() { /* ... */ } // Not descriptive
function DoStuff() { /* ... */ } // Too vague
function UserProfileCardComponentThatDisplaysUserInformation() { /* Too long */ }
Component Organization
Organize components logically in your project:
src/
âââ components/ # Reusable components
â âââ Button/
â â âââ Button.tsx
â â âââ Button.css
â â âââ Button.test.tsx
â âââ Card/
â â âââ Card.tsx
â â âââ Card.css
â âââ Modal/
â âââ Modal.tsx
â âââ Modal.css
âââ pages/ # Page-level components
â âââ Home.tsx
â âââ About.tsx
â âââ Contact.tsx
âââ layouts/ # Layout components
â âââ Header.tsx
â âââ Footer.tsx
â âââ Sidebar.tsx
âââ App.tsx
Exporting Components
Two ways to export components:
// Default export (one per file)
export default function Button() {
return <button>Click</button>;
}
// Import: can use any name
import MyButton from './Button';
import Btn from './Button';
// ---
// Named export (multiple per file)
export function PrimaryButton() {
return <button className="primary">Primary</button>;
}
export function SecondaryButton() {
return <button className="secondary">Secondary</button>;
}
// Import: must use exact names
import { PrimaryButton, SecondaryButton } from './Buttons';
// Can rename with 'as'
import { PrimaryButton as MainButton } from './Buttons';
â When to Use Each
Default Export: When file contains one main component
Named Export: When file contains multiple related components or utilities
Using Components
Once created, use components like HTML tags:
import Button from './components/Button';
import { PrimaryButton, SecondaryButton } from './components/Buttons';
function App() {
return (
<div>
{/* Self-closing if no children */}
<Button />
{/* With children */}
<Button>Click me</Button>
{/* Multiple times */}
<PrimaryButton />
<SecondaryButton />
<PrimaryButton />
</div>
);
}
đĻ Props Fundamentals
Props (properties) are how you pass data from parent to child components. They're the communication channel that makes components reusable!
đ Definition
Props: Arguments passed to components, similar to function parameters. They allow parent components to pass data and behavior to child components. Props are read-only and flow in one direction (parent to child).
Basic Props Usage
Passing and receiving props is straightforward:
// Component that receives props
function Greeting(props) {
return <h1>Hello, {props.name}!</h1>;
}
// Parent component passing props
function App() {
return (
<div>
<Greeting name="Alice" />
<Greeting name="Bob" />
<Greeting name="Charlie" />
</div>
);
}
// Output:
// Hello, Alice!
// Hello, Bob!
// Hello, Charlie!
Destructuring Props
Destructuring makes your code cleaner and more readable:
// Without destructuring
function UserCard(props) {
return (
<div>
<h2>{props.name}</h2>
<p>{props.email}</p>
<p>{props.age} years old</p>
</div>
);
}
// With destructuring (Better!)
function UserCard({ name, email, age }) {
return (
<div>
<h2>{name}</h2>
<p>{email}</p>
<p>{age} years old</p>
</div>
);
}
// Usage
<UserCard name="Alice" email="alice@example.com" age={30} />
Types of Prop Values
Props can be any JavaScript value:
function PropsExample() {
const user = { name: "Alice", age: 30 };
const handleClick = () => console.log("Clicked!");
return (
<MyComponent
// String (quotes or curly braces)
text="Hello"
title={"World"}
// Number (must use curly braces)
count={42}
max={100}
// Boolean
isActive={true}
disabled={false}
enabled {/* true if present */}
// Array
items={[1, 2, 3, 4, 5]}
names={["Alice", "Bob"]}
// Object
user={user}
settings={{ theme: 'dark', lang: 'en' }}
// Function
onClick={handleClick}
onSubmit={() => console.log("Submit")}
// JSX
icon={<span>â</span>}
header={<h2>Title</h2>}
/>
);
}
Props Are Read-Only
A fundamental rule: never modify props inside a component!
function Counter({ count }) {
// â WRONG - Never modify props!
count = count + 1; // This won't work and breaks React's model
return <p>Count: {count}</p>;
}
// â
CORRECT - Use state if you need to modify values
import { useState } from 'react';
function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
const increment = () => {
setCount(count + 1); // Modify state, not props
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
â ī¸ Why Are Props Read-Only?
Props flow from parent to child. If a child could modify its props, it would be changing data that belongs to the parent - chaos! This one-way data flow makes React predictable and easier to debug. If you need to modify data, use state instead.
Passing Functions as Props
A common pattern is passing functions from parent to child:
// Parent component
function TodoApp() {
const [todos, setTodos] = useState([
{ id: 1, text: "Learn React", done: false },
{ id: 2, text: "Build a project", done: false }
]);
// Function to handle todo completion
const handleToggle = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
};
return (
<div>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle} {/* Pass function as prop */}
/>
))}
</div>
);
}
// Child component
function TodoItem({ todo, onToggle }) {
return (
<div>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)} {/* Call parent function */}
/>
<span>{todo.text}</span>
</div>
);
}
đ¨ Interactive: Props Flow Visualizer
Watch how props flow from parent to child! Change values in the parent to see them update in the child:
Props vs State
Understanding the difference is crucial:
| Feature | Props | State |
|---|---|---|
| Source | Passed from parent | Managed by component |
| Mutability | Read-only | Can be updated |
| Purpose | Configure component | Track changing data |
| Updates | Parent re-renders child | setState triggers re-render |
| Example | User name, button text | Form values, toggle state |
đ¨ Interactive: Props vs State in Action
See the difference! Props come from outside (parent), State lives inside the component:
đˇ TypeScript and Props
TypeScript brings type safety to props, catching errors before they happen and making your components self-documenting!
Defining Prop Types with Interfaces
Always define interfaces for your props:
// Define the shape of props
interface UserCardProps {
name: string;
email: string;
age: number;
isActive: boolean;
}
// Use the interface
function UserCard({ name, email, age, isActive }: UserCardProps) {
return (
<div className="user-card">
<h2>{name}</h2>
<p>{email}</p>
<p>{age} years old</p>
{isActive && <span className="badge">Active</span>}
</div>
);
}
// TypeScript ensures correct usage
<UserCard
name="Alice"
email="alice@example.com"
age={30}
isActive={true}
/> // â
Correct
<UserCard name="Bob" /> // â Error: missing required props
Optional Props
Use ? to make props optional:
interface ButtonProps {
text: string; // Required
onClick: () => void; // Required
disabled?: boolean; // Optional
className?: string; // Optional
icon?: React.ReactNode; // Optional
}
function Button({ text, onClick, disabled, className, icon }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={className}
>
{icon && <span className="icon">{icon}</span>}
{text}
</button>
);
}
// All valid uses
<Button text="Click" onClick={() => {}} />
<Button text="Submit" onClick={() => {}} disabled={true} />
<Button text="Save" onClick={() => {}} className="primary" icon="đž" />
Complex Prop Types
Props can have sophisticated types:
// Union types
interface AlertProps {
type: 'success' | 'error' | 'warning' | 'info';
message: string;
}
// Nested objects
interface User {
id: number;
name: string;
contact: {
email: string;
phone?: string;
};
}
interface UserProfileProps {
user: User;
}
// Arrays
interface TodoListProps {
todos: Array<{
id: number;
text: string;
completed: boolean;
}>;
}
// Or with interface
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoListProps {
todos: Todo[];
}
// Function types
interface FormProps {
onSubmit: (data: { name: string; email: string }) => void;
onCancel: () => void;
onError?: (error: Error) => void;
}
// Generic types
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item)}</li>
))}
</ul>
);
}
â TypeScript Benefits for Props
- Autocomplete - IDE suggests available props
- Error catching - Typos caught at compile time
- Documentation - Interface shows what props exist
- Refactoring - Rename props safely across codebase
- Confidence - Know your code works before running it
Extending Props
Extend interfaces to build on existing prop definitions:
// Base button props
interface BaseButtonProps {
text: string;
onClick: () => void;
disabled?: boolean;
}
// Icon button extends base
interface IconButtonProps extends BaseButtonProps {
icon: string;
iconPosition?: 'left' | 'right';
}
// Loading button extends base
interface LoadingButtonProps extends BaseButtonProps {
isLoading: boolean;
loadingText?: string;
}
function IconButton({ text, onClick, disabled, icon, iconPosition = 'left' }: IconButtonProps) {
return (
<button onClick={onClick} disabled={disabled}>
{iconPosition === 'left' && <span>{icon}</span>}
{text}
{iconPosition === 'right' && <span>{icon}</span>}
</button>
);
}
Extending HTML Element Props
Extend built-in HTML element props:
// Extend button element props
interface CustomButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary';
}
function CustomButton({ variant = 'primary', children, ...rest }: CustomButtonProps) {
return (
<button
className={`btn btn-${variant}`}
{...rest} {/* Spread remaining HTML button props */}
>
{children}
</button>
);
}
// Now supports all button props plus custom variant
<CustomButton
variant="primary"
onClick={() => {}}
disabled={false}
type="submit"
aria-label="Submit form"
>
Submit
</CustomButton>
đļ Children Prop
The children prop is special - it represents the content between opening and closing tags of a component. It's the secret to making truly flexible, composable components!
đ Definition
Children Prop: A special prop automatically passed to components containing the content between the component's opening and closing tags. It enables component composition and makes components act like HTML containers.
Basic Children Usage
Children makes components work like HTML elements:
// Component that uses children
function Card({ children }) {
return (
<div className="card">
{children}
</div>
);
}
// Using the component
function App() {
return (
<Card>
<h2>Card Title</h2>
<p>Card content goes here</p>
</Card>
);
}
Children with TypeScript
Type the children prop properly:
// Method 1: Using React.ReactNode (recommended)
interface CardProps {
children: React.ReactNode;
}
function Card({ children }: CardProps) {
return <div className="card">{children}</div>;
}
// Method 2: Using PropsWithChildren helper
import { PropsWithChildren } from 'react';
interface CardProps {
title: string;
}
function Card({ title, children }: PropsWithChildren<CardProps>) {
return (
<div className="card">
<h2>{title}</h2>
{children}
</div>
);
}
// Usage
<Card title="My Card">
<p>Any content here!</p>
</Card>
Children Can Be Anything
Children accepts all types of React content:
function Container({ children }: { children: React.ReactNode }) {
return <div className="container">{children}</div>;
}
// String children
<Container>Hello World</Container>
// JSX children
<Container>
<h1>Title</h1>
<p>Paragraph</p>
</Container>
// Component children
<Container>
<Header />
<Main />
<Footer />
</Container>
// Expression children
<Container>
{items.map(item => <div key={item.id}>{item.name}</div>)}
</Container>
// Mixed children
<Container>
<h1>Products</h1>
{products.map(p => <ProductCard key={p.id} product={p} />)}
<Button>Load More</Button>
</Container>
Layout Components with Children
Children makes layout components incredibly powerful:
// Modal component
interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
function Modal({ isOpen, onClose, children }: ModalProps) {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<button className="modal-close" onClick={onClose}>Ã</button>
{children}
</div>
</div>
);
}
// Usage - put any content inside
function App() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>Open Modal</button>
<Modal isOpen={showModal} onClose={() => setShowModal(false)}>
<h2>Confirm Action</h2>
<p>Are you sure you want to proceed?</p>
<button onClick={() => setShowModal(false)}>Yes</button>
<button onClick={() => setShowModal(false)}>No</button>
</Modal>
</div>
);
}
Multiple Children Slots
Sometimes you want multiple areas for children:
// Instead of just children, use named props
interface PageLayoutProps {
header: React.ReactNode;
sidebar: React.ReactNode;
main: React.ReactNode;
footer: React.ReactNode;
}
function PageLayout({ header, sidebar, main, footer }: PageLayoutProps) {
return (
<div className="page-layout">
<header className="header">{header}</header>
<div className="content-area">
<aside className="sidebar">{sidebar}</aside>
<main className="main">{main}</main>
</div>
<footer className="footer">{footer}</footer>
</div>
);
}
// Usage
<PageLayout
header={<h1>My Site</h1>}
sidebar={<Navigation />}
main={<Article />}
footer={<Copyright />}
/>
Children with Additional Props
Combine children with other props:
interface AlertProps {
type: 'success' | 'error' | 'warning' | 'info';
title: string;
children: React.ReactNode;
onClose?: () => void;
}
function Alert({ type, title, children, onClose }: AlertProps) {
const icons = {
success: 'â
',
error: 'â',
warning: 'â ī¸',
info: 'âšī¸'
};
return (
<div className={`alert alert-${type}`}>
<div className="alert-header">
<span className="alert-icon">{icons[type]}</span>
<h3>{title}</h3>
{onClose && (
<button onClick={onClose} className="alert-close">Ã</button>
)}
</div>
<div className="alert-body">
{children}
</div>
</div>
);
}
// Usage
<Alert type="success" title="Success!">
<p>Your changes have been saved.</p>
<button>View Details</button>
</Alert>
đĄ Children vs Regular Props
Use children when:
- Content is the main purpose of the component
- Content varies significantly between uses
- You want HTML-like component syntax
Use regular props when:
- You need multiple content areas
- Content is simple (strings, numbers)
- You want clear, descriptive names
đ¨ Interactive: Children Prop Visualizer
See how the children prop works! Content between opening and closing tags becomes the children:
Manipulating Children
React provides utilities to work with children:
import { Children, isValidElement, cloneElement } from 'react';
interface ListProps {
children: React.ReactNode;
}
function List({ children }: ListProps) {
// Count children
const count = Children.count(children);
// Map over children
const items = Children.map(children, (child, index) => {
// Check if child is a valid React element
if (isValidElement(child)) {
// Clone and add props
return cloneElement(child, {
key: index,
className: 'list-item'
});
}
return child;
});
return (
<div>
<p>{count} items</p>
<ul>{items}</ul>
</div>
);
}
â ī¸ Manipulating Children is Advanced
For most use cases, just render children directly. Manipulating children is powerful but can be complex. Reserve it for building component libraries or very special cases.
đ¯ Default Props
Default props let you specify fallback values when props aren't provided. This makes components easier to use and reduces repetitive code!
Default Parameters (Modern Approach)
Use JavaScript's default parameters - simple and TypeScript-friendly:
interface ButtonProps {
text: string;
variant?: 'primary' | 'secondary';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
}
function Button({
text,
variant = 'primary', // Default value
size = 'medium', // Default value
disabled = false // Default value
}: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
>
{text}
</button>
);
}
// All these work
<Button text="Click me" />
// Uses: variant="primary", size="medium", disabled=false
<Button text="Submit" variant="secondary" />
// Uses: variant="secondary", size="medium", disabled=false
<Button text="Small" size="small" disabled={true} />
// Uses: variant="primary", size="small", disabled=true
Complex Default Values
Defaults can be objects, arrays, or functions:
interface UserProfileProps {
user?: {
name: string;
email: string;
};
settings?: {
theme: string;
language: string;
};
onSave?: (data: any) => void;
}
function UserProfile({
user = { name: 'Guest', email: 'guest@example.com' },
settings = { theme: 'light', language: 'en' },
onSave = () => console.log('No save handler provided')
}: UserProfileProps) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>Theme: {settings.theme}</p>
<button onClick={() => onSave({ user, settings })}>
Save
</button>
</div>
);
}
Conditional Defaults
Use nullish coalescing for more control:
interface MessageProps {
title?: string;
content?: string;
timestamp?: Date;
}
function Message({ title, content, timestamp }: MessageProps) {
// Use ?? for defaults
const displayTitle = title ?? 'Untitled';
const displayContent = content ?? 'No content available';
const displayTime = timestamp ?? new Date();
return (
<div className="message">
<h3>{displayTitle}</h3>
<p>{displayContent}</p>
<small>{displayTime.toLocaleString()}</small>
</div>
);
}
defaultProps (Legacy)
You might see this in older code:
// Old way (still works but not recommended)
interface ButtonProps {
text: string;
variant?: string;
}
function Button({ text, variant }: ButtonProps) {
return <button className={variant}>{text}</button>;
}
Button.defaultProps = {
variant: 'primary'
};
// Modern way (preferred)
function Button({ text, variant = 'primary' }: ButtonProps) {
return <button className={variant}>{text}</button>;
}
â Best Practices for Defaults
- Use default parameters - Modern, clean, TypeScript-friendly
- Keep defaults simple - Avoid complex computations
- Document defaults - Make it clear what the fallback is
- Consider required props - Not everything needs a default
đ§Š Component Composition
Composition is about building complex components from simpler ones. It's one of React's most powerful patterns!
Composition Basics
Instead of inheritance, React uses composition:
// Small, focused components
function Avatar({ src, alt }: { src: string; alt: string }) {
return <img className="avatar" src={src} alt={alt} />;
}
function UserName({ name }: { name: string }) {
return <h3 className="username">{name}</h3>;
}
function UserBio({ bio }: { bio: string }) {
return <p className="bio">{bio}</p>;
}
// Compose them into a larger component
function UserCard({ user }: { user: any }) {
return (
<div className="user-card">
<Avatar src={user.avatar} alt={user.name} />
<UserName name={user.name} />
<UserBio bio={user.bio} />
</div>
);
}
Container and Presentational Pattern
Separate logic from presentation:
// Presentational component (how it looks)
interface TodoItemProps {
todo: {
id: number;
text: string;
completed: boolean;
};
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}
function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
return (
<li className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
}
// Container component (how it works)
function TodoListContainer() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', completed: false },
{ id: 2, text: 'Build a project', completed: false }
]);
const handleToggle = (id: number) => {
setTodos(todos.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
));
};
const handleDelete = (id: number) => {
setTodos(todos.filter(t => t.id !== id));
};
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
);
}
Wrapper Components
Create components that wrap and enhance others:
// Generic wrapper that adds border
interface BorderedProps {
children: React.ReactNode;
color?: string;
width?: number;
}
function Bordered({ children, color = '#ccc', width = 1 }: BorderedProps) {
return (
<div style={{ border: `${width}px solid ${color}`, padding: '1rem' }}>
{children}
</div>
);
}
// Wrapper that adds padding
function Padded({ children }: { children: React.ReactNode }) {
return <div style={{ padding: '2rem' }}>{children}</div>;
}
// Compose wrappers
function MyComponent() {
return (
<Bordered color="blue">
<Padded>
<h1>Title</h1>
<p>Content</p>
</Padded>
</Bordered>
);
}
Specialized Components
Create specialized versions of generic components:
// Generic Dialog component
interface DialogProps {
title: string;
children: React.ReactNode;
onClose: () => void;
actions: React.ReactNode;
}
function Dialog({ title, children, onClose, actions }: DialogProps) {
return (
<div className="dialog">
<div className="dialog-header">
<h2>{title}</h2>
<button onClick={onClose}>Ã</button>
</div>
<div className="dialog-body">{children}</div>
<div className="dialog-actions">{actions}</div>
</div>
);
}
// Specialized confirmation dialog
interface ConfirmDialogProps {
message: string;
onConfirm: () => void;
onCancel: () => void;
}
function ConfirmDialog({ message, onConfirm, onCancel }: ConfirmDialogProps) {
return (
<Dialog
title="Confirm Action"
onClose={onCancel}
actions={
<>
<button onClick={onCancel}>Cancel</button>
<button onClick={onConfirm}>Confirm</button>
</>
}
>
<p>{message}</p>
</Dialog>
);
}
// Easy to use!
<ConfirmDialog
message="Are you sure you want to delete this item?"
onConfirm={() => deleteItem()}
onCancel={() => closeDialog()}
/>
đĄ Composition vs Inheritance
React recommends composition over inheritance. Instead of creating a class hierarchy, compose simpler components into complex ones. This leads to more flexible, reusable code.
// â Don't: Inheritance (not the React way)
class BaseButton extends React.Component { }
class PrimaryButton extends BaseButton { }
// â
Do: Composition
function Button({ variant, children }) {
return <button className={`btn-${variant}`}>{children}</button>;
}
function PrimaryButton({ children }) {
return <Button variant="primary">{children}</Button>;
}
đ¨ Interactive: Component Composition Explorer
Build a component from smaller pieces! Click to add or remove sub-components:
đ¨ Component Patterns
Learn proven patterns that make your components more flexible, reusable, and maintainable!
Render Props Pattern
Pass a function as a prop that returns React elements:
interface MouseTrackerProps {
render: (position: { x: number; y: number }) => React.ReactNode;
}
function MouseTracker({ render }: MouseTrackerProps) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e: React.MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
return (
<div onMouseMove={handleMouseMove} style={{ height: '100vh' }}>
{render(position)}
</div>
);
}
// Usage - flexible rendering!
function App() {
return (
<MouseTracker
render={({ x, y }) => (
<div>
<h2>Move your mouse</h2>
<p>Position: {x}, {y}</p>
</div>
)}
/>
);
}
Compound Components Pattern
Components that work together to form a cohesive API:
// Tabs component system
interface TabsContextType {
activeTab: string;
setActiveTab: (id: string) => void;
}
const TabsContext = React.createContext<TabsContextType | null>(null);
function Tabs({ children, defaultTab }: { children: React.ReactNode; defaultTab: string }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }: { children: React.ReactNode }) {
return <div className="tab-list">{children}</div>;
}
function Tab({ id, children }: { id: string; children: React.ReactNode }) {
const context = React.useContext(TabsContext);
if (!context) throw new Error('Tab must be used within Tabs');
return (
<button
className={context.activeTab === id ? 'active' : ''}
onClick={() => context.setActiveTab(id)}
>
{children}
</button>
);
}
function TabPanel({ id, children }: { id: string; children: React.ReactNode }) {
const context = React.useContext(TabsContext);
if (!context) throw new Error('TabPanel must be used within Tabs');
return context.activeTab === id ? <div>{children}</div> : null;
}
// Beautiful, declarative usage!
<Tabs defaultTab="home">
<TabList>
<Tab id="home">Home</Tab>
<Tab id="profile">Profile</Tab>
<Tab id="settings">Settings</Tab>
</TabList>
<TabPanel id="home">Home content</TabPanel>
<TabPanel id="profile">Profile content</TabPanel>
<TabPanel id="settings">Settings content</TabPanel>
</Tabs>
Higher-Order Component (HOC) Pattern
A function that takes a component and returns a new component:
// HOC that adds loading state
function withLoading<P extends object>(
Component: React.ComponentType<P>
) {
return function WithLoadingComponent(
props: P & { isLoading: boolean }
) {
const { isLoading, ...rest } = props;
if (isLoading) {
return <div>Loading...</div>;
}
return <Component {...rest as P} />;
};
}
// Original component
interface UserListProps {
users: Array<{ name: string }>;
}
function UserList({ users }: UserListProps) {
return (
<ul>
{users.map(u => <li key={u.name}>{u.name}</li>)}
</ul>
);
}
// Enhanced component
const UserListWithLoading = withLoading(UserList);
// Usage
<UserListWithLoading users={users} isLoading={loading} />
Controlled vs Uncontrolled Pattern
Components can control their own state or be controlled by parent:
// Controlled component - state lives in parent
interface ControlledInputProps {
value: string;
onChange: (value: string) => void;
}
function ControlledInput({ value, onChange }: ControlledInputProps) {
return (
<input
value={value}
onChange={(e) => onChange(e.target.value)}
/>
);
}
// Uncontrolled component - state lives internally
function UncontrolledInput() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
// Hybrid - can be either!
interface FlexibleInputProps {
value?: string;
defaultValue?: string;
onChange?: (value: string) => void;
}
function FlexibleInput({ value: propValue, defaultValue = '', onChange }: FlexibleInputProps) {
const [internalValue, setInternalValue] = useState(defaultValue);
// Controlled if value prop provided
const isControlled = propValue !== undefined;
const value = isControlled ? propValue : internalValue;
const handleChange = (newValue: string) => {
if (!isControlled) {
setInternalValue(newValue);
}
onChange?.(newValue);
};
return (
<input
value={value}
onChange={(e) => handleChange(e.target.value)}
/>
);
}
â When to Use Each Pattern
- Render Props: Share behavior across components
- Compound Components: Related components that work together
- HOCs: Add cross-cutting concerns (less common in modern React)
- Controlled: Parent needs to know/control the state
- Uncontrolled: Simple, self-contained components
đī¸ Hands-on Practice
đī¸ Exercise 1: Build a Reusable Card System
Objective: Create a flexible Card component with various layouts using composition.
Requirements:
- Create a base
Cardcomponent with children - Create
CardHeader,CardBody, andCardFootercomponents - Support optional props for styling (variant, padding, shadow)
- Make components composable and reusable
- Add TypeScript interfaces for all props
Starter Code:
// TODO: Create Card component
interface CardProps {
// Define props
}
function Card({ }: CardProps) {
return <div></div>;
}
// TODO: Create CardHeader component
// TODO: Create CardBody component
// TODO: Create CardFooter component
// Usage example:
function App() {
return (
<Card>
<CardHeader>
<h2>Product Name</h2>
</CardHeader>
<CardBody>
<p>Product description goes here.</p>
</CardBody>
<CardFooter>
<button>Add to Cart</button>
</CardFooter>
</Card>
);
}
đĄ Hint
Use the children prop for each sub-component. Add optional className props for custom styling. Consider using a variant prop for different card styles (primary, secondary, danger).
â Solution
import { ReactNode } from 'react';
import './Card.css';
// Card Component
type CardVariant = 'default' | 'primary' | 'secondary' | 'danger';
interface CardProps {
children: ReactNode;
variant?: CardVariant;
padding?: 'none' | 'small' | 'medium' | 'large';
shadow?: boolean;
className?: string;
}
function Card({
children,
variant = 'default',
padding = 'medium',
shadow = true,
className = ''
}: CardProps) {
const classes = [
'card',
`card--${variant}`,
`card--padding-${padding}`,
shadow && 'card--shadow',
className
].filter(Boolean).join(' ');
return (
<div className={classes}>
{children}
</div>
);
}
// CardHeader Component
interface CardHeaderProps {
children: ReactNode;
className?: string;
}
function CardHeader({ children, className = '' }: CardHeaderProps) {
return (
<div className={`card__header ${className}`}>
{children}
</div>
);
}
// CardBody Component
interface CardBodyProps {
children: ReactNode;
className?: string;
}
function CardBody({ children, className = '' }: CardBodyProps) {
return (
<div className={`card__body ${className}`}>
{children}
</div>
);
}
// CardFooter Component
interface CardFooterProps {
children: ReactNode;
align?: 'left' | 'center' | 'right';
className?: string;
}
function CardFooter({
children,
align = 'right',
className = ''
}: CardFooterProps) {
return (
<div className={`card__footer card__footer--${align} ${className}`}>
{children}
</div>
);
}
// Export all components
export { Card, CardHeader, CardBody, CardFooter };
// Usage Example
function ProductCard() {
return (
<Card variant="primary" shadow>
<CardHeader>
<h2>Premium Widget</h2>
<span className="badge">New</span>
</CardHeader>
<CardBody>
<img src="/product.jpg" alt="Product" />
<p>High-quality widget with amazing features.</p>
<p className="price">$99.99</p>
</CardBody>
<CardFooter align="center">
<button className="btn btn-primary">Add to Cart</button>
<button className="btn btn-secondary">Details</button>
</CardFooter>
</Card>
);
}
/* Card.css */
.card {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background: white;
}
.card--shadow {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.card--primary {
border-color: #667eea;
}
.card--secondary {
border-color: #6c757d;
}
.card--danger {
border-color: #dc3545;
}
.card--padding-none {
padding: 0;
}
.card--padding-small {
padding: 0.5rem;
}
.card--padding-medium {
padding: 1rem;
}
.card--padding-large {
padding: 2rem;
}
.card__header {
padding: 1rem;
border-bottom: 1px solid #e0e0e0;
background: #f8f9fa;
}
.card__body {
padding: 1rem;
}
.card__footer {
padding: 1rem;
border-top: 1px solid #e0e0e0;
background: #f8f9fa;
display: flex;
gap: 0.5rem;
}
.card__footer--left {
justify-content: flex-start;
}
.card__footer--center {
justify-content: center;
}
.card__footer--right {
justify-content: flex-end;
}
đī¸ Exercise 2: Create a Button Component Library
Objective: Build a type-safe Button component with variants, sizes, and states.
Requirements:
- Support variants: primary, secondary, danger, ghost
- Support sizes: small, medium, large
- Support disabled and loading states
- Accept an onClick handler
- Use TypeScript union types for variants and sizes
â Solution
import { ReactNode } from 'react';
import './Button.css';
type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
type ButtonSize = 'small' | 'medium' | 'large';
interface ButtonProps {
children: ReactNode;
variant?: ButtonVariant;
size?: ButtonSize;
disabled?: boolean;
loading?: boolean;
onClick?: () => void;
type?: 'button' | 'submit' | 'reset';
className?: string;
}
function Button({
children,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
onClick,
type = 'button',
className = ''
}: ButtonProps) {
const classes = [
'btn',
`btn--${variant}`,
`btn--${size}`,
loading && 'btn--loading',
className
].filter(Boolean).join(' ');
const handleClick = () => {
if (!disabled && !loading && onClick) {
onClick();
}
};
return (
<button
type={type}
className={classes}
disabled={disabled || loading}
onClick={handleClick}
>
{loading && <span className="btn__spinner">âŗ</span>}
<span className="btn__content">{children}</span>
</button>
);
}
export default Button;
// Usage Examples
function Examples() {
return (
<div>
<Button variant="primary" size="large">
Large Primary
</Button>
<Button variant="secondary" size="medium">
Medium Secondary
</Button>
<Button variant="danger" size="small">
Small Danger
</Button>
<Button variant="ghost" disabled>
Disabled
</Button>
<Button loading onClick={() => console.log('Clicked')}>
Loading...
</Button>
</div>
);
}
đ¯ Quick Quiz
Question 1: What are props in React?
Question 2: How do you make a prop optional in TypeScript?
Question 3: What is the children prop used for?
đ Best Practices
â Component Design Do's
- Keep components small and focused - One component, one responsibility
- Use TypeScript interfaces for props - Document and validate your component API
- Destructure props in parameters - Makes code cleaner and more readable
- Provide default prop values - Make components easier to use with sensible defaults
- Use meaningful prop names -
isActivenotactive,onSubmitnotsubmit - Extract reusable components - DRY (Don't Repeat Yourself)
- Use composition over configuration - Multiple simple components beat one complex component
- Make components predictable - Same props should always produce same output
â Component Design Don'ts
- Don't mutate props - Props are read-only, always
- Don't put too many props on one component - If you have 10+ props, break it down
- Don't use generic names -
Buttonis better thanComponent1 - Don't make components do too much - Split complex components into smaller ones
- Don't use any type for props - Define proper TypeScript interfaces
- Don't forget to export components - Use default or named exports consistently
- Don't create deeply nested component trees - Flatten when possible
đĄ Pro Tips
- Co-locate related files - Keep component, styles, and tests together
- Use prop spreading wisely - Good for forwarding props, but can hide dependencies
- Document complex props - Add JSDoc comments for tricky interfaces
- Consider prop drilling - If passing props through many levels, consider Context
- Use React.FC sparingly - Plain function components are preferred in modern React
- Name event handlers consistently - Use
handle*for internal,on*for props
Component Organization Checklist
// 1. Imports
import { ReactNode, useState } from 'react';
import { SomeOtherComponent } from './SomeOtherComponent';
import './MyComponent.css';
// 2. Type definitions
interface MyComponentProps {
title: string;
count?: number;
onAction?: () => void;
}
// 3. Component definition
function MyComponent({
title,
count = 0,
onAction
}: MyComponentProps) {
// 4. Hooks
const [isActive, setIsActive] = useState(false);
// 5. Event handlers
const handleClick = () => {
setIsActive(!isActive);
onAction?.();
};
// 6. Derived values
const displayCount = count > 99 ? '99+' : count;
// 7. Early returns
if (!title) {
return null;
}
// 8. Main render
return (
<div className="my-component">
<h2>{title}</h2>
<p>{displayCount}</p>
<button onClick={handleClick}>
{isActive ? 'Active' : 'Inactive'}
</button>
</div>
);
}
// 9. Export
export default MyComponent;
Props Naming Conventions
| Type | Naming Pattern | Example |
|---|---|---|
| Boolean | is*, has*, should* | isActive, hasError, shouldShow |
| Event Handlers | on* | onClick, onSubmit, onClose |
| Render Props | render* | renderHeader, renderItem |
| Count/Number | *Count, num*, total* | itemCount, numPages, totalPrice |
| Collections | Plural nouns | items, users, products |
đ Summary
đ Congratulations!
You've mastered React components and props! Here's what you now know:
- Components - Self-contained, reusable UI pieces
- Function components - Modern React component syntax
- Props - Read-only data passed from parent to child
- TypeScript interfaces - Type-safe prop definitions
- Optional props - Using
?and default values - Children prop - Content between component tags
- Component composition - Building complex UIs from simple parts
- Design patterns - Compound components, render props, controlled/uncontrolled
- Best practices - Writing clean, maintainable components
đ¯ Key Takeaways
- Components are functions that return JSX
- Props make components flexible and reusable
- Props are read-only - never modify them
- TypeScript interfaces provide type safety
- Children prop enables composition
- Keep components small and focused
- Composition beats configuration
- Name components and props meaningfully
Component Lifecycle Recap
đ Additional Resources
- React Docs: Passing Props to a Component
- React Docs: Your First Component
- React TypeScript: Function Components
- React Docs: Thinking in React
đ What's Next?
In the next lesson, we'll explore Styling in React. You'll learn:
- CSS Modules for scoped styles
- Inline styles and when to use them
- CSS-in-JS solutions
- Styling libraries (Tailwind, styled-components)
- Conditional styling techniques
- Responsive design in React
You can now build reusable components - let's make them beautiful! đ¨
đ Component Master Achieved!
You now understand the fundamental building blocks of React! You can create reusable components, pass data with props, and compose complex UIs from simple pieces. This is the foundation everything else builds on!
Ready to style your components! đ