Skip to main content

🧩 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:

graph TD A[App] --> B[Header] A --> C[MainContent] A --> D[Footer] B --> E[Logo] B --> F[Navigation] F --> G[NavLink] F --> H[NavLink] F --> I[NavLink] C --> J[Sidebar] C --> K[ArticleList] K --> L[Article] K --> M[Article] K --> N[Article] L --> O[Title] L --> P[Content] L --> Q[Comments] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style C fill:#4CAF50,stroke:#333,stroke-width:2px

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, not userProfile
  • Descriptive names - LoginButton vs Button1
  • File name matches component - Button.tsx contains Button
  • 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>
    );
}
graph TD A[Parent Component] -->|Props down| B[Child Component] B -->|Callback function| A A -->|Data: name, age| B B -->|Event: onClick| A style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style B fill:#4CAF50,stroke:#333,stroke-width:2px

🎨 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 Flow: Parent → Child Parent Component const name = "Alice"; const age = 25; const active = true; Props: name, age, active Child Component function Child({ name, age, active }) Renders: Alice, 25, active onClick() Change the parent values to see props 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:

The Children Prop How You Write It <Card> <h2>Title</h2> <p>Content here</p> <Button /> </Card> What Component Receives function Card({ children }) {'{'}} // children = <h2>Title</h2> <p>Content here</p> <Button /> {'}'} Rendered Result (Card wraps children) Title | Content here | [Button] Click buttons to change what children are passed!

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>;
}
graph TD A[Page] --> B[Header] A --> C[Content] A --> D[Sidebar] B --> E[Logo] B --> F[Nav] C --> G[Article] C --> H[Comments] G --> I[Title] G --> J[Body] G --> K[Author] H --> L[Comment] H --> M[Comment] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style C fill:#4CAF50,stroke:#333,stroke-width:2px

🎨 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:

  1. Create a base Card component with children
  2. Create CardHeader, CardBody, and CardFooter components
  3. Support optional props for styling (variant, padding, shadow)
  4. Make components composable and reusable
  5. 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:

  1. Support variants: primary, secondary, danger, ghost
  2. Support sizes: small, medium, large
  3. Support disabled and loading states
  4. Accept an onClick handler
  5. 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 - isActive not active, onSubmit not submit
  • 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 - Button is better than Component1
  • 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

graph LR A[Define Props Interface] --> B[Create Component Function] B --> C[Destructure Props] C --> D[Add Logic/Hooks] D --> E[Return JSX] E --> F[Export Component] F --> G[Import & Use with Props] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style G fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff

📚 Additional Resources

🚀 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! 🚀