Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
16 minLesson 3 of 33
React Core Concepts

Props & Component Composition

Props & Component Composition

Props are how components communicate. Master the patterns in this lesson and you'll be able to build any component API — flexible enough for edge cases, simple enough for daily use.

Props Fundamentals

Props are plain JavaScript values passed from parent to child. Any type works: strings, numbers, arrays, objects, functions, even other components:

// String, number, boolean, array
<ProductCard
    name="React Course"
    price={49}
    isBestseller={true}
    tags={["react", "javascript", "frontend"]}
/>

// Object
<UserAvatar user={{ name: "Alice", avatarUrl: "/alice.jpg", role: "admin" }} />

// Function (callback)
<Button onClick={() => setOpen(true)}>Open Modal</Button>

// Component (render prop)
<Modal header={<h2>Confirm Delete</h2>}>
    <p>This cannot be undone.</p>
</Modal>

Destructuring Props

Always destructure — it's cleaner and shows exactly what a component needs:

// ❌ Verbose
function Card(props: CardProps) {
    return <div>{props.title} — {props.subtitle}</div>;
}

// ✅ Destructured
function Card({ title, subtitle, children }: CardProps) {
    return <div>{title} — {subtitle}{children}</div>;
}

// With defaults
function Button({ 
    children, 
    variant = "primary",
    size = "md",
    disabled = false,
    onClick 
}: ButtonProps) {
    // ...
}

The children Prop

children is the JSX content placed between a component's opening and closing tags. It lets you build wrapper components:

interface CardProps {
    title: string;
    className?: string;
    children: React.ReactNode;
}

function Card({ title, className = "", children }: CardProps) {
    return (
        <div className={`bg-white rounded-xl border border-gray-200 shadow-sm ${className}`}>
            <div className="px-6 py-4 border-b border-gray-100">
                <h3 className="font-semibold text-gray-900">{title}</h3>
            </div>
            <div className="p-6">
                {children}
            </div>
        </div>
    );
}

// Usage — anything goes inside:
<Card title="User Details">
    <div className="flex items-center gap-3">
        <Avatar user={user} />
        <div>
            <p className="font-medium">{user.name}</p>
            <p className="text-gray-500 text-sm">{user.email}</p>
        </div>
    </div>
</Card>

Composition Over Configuration

Instead of adding more props to handle more cases, use composition:

// ❌ Configuration approach — gets messy fast
<Modal
    title="Delete Course"
    body="Are you sure?"
    primaryButtonLabel="Delete"
    primaryButtonVariant="danger"
    secondaryButtonLabel="Cancel"
    showIcon={true}
    iconType="warning"
    onPrimary={handleDelete}
    onSecondary={() => setOpen(false)}
/>

// ✅ Composition approach — flexible and readable
<Modal>
    <Modal.Header>
        <WarningIcon className="text-red-500" />
        <Modal.Title>Delete Course</Modal.Title>
    </Modal.Header>
    <Modal.Body>
        <p>Are you sure? This action cannot be undone.</p>
    </Modal.Body>
    <Modal.Footer>
        <Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
        <Button variant="danger" onClick={handleDelete}>Delete</Button>
    </Modal.Footer>
</Modal>

The second API can handle any content in any arrangement. The first needs a new prop every time you encounter an edge case.

Slot Pattern

Pass named sections as props — useful when children isn't enough:

interface PageLayoutProps {
    header: React.ReactNode;
    sidebar: React.ReactNode;
    children: React.ReactNode;
}

function PageLayout({ header, sidebar, children }: PageLayoutProps) {
    return (
        <div className="min-h-screen flex flex-col">
            <header className="h-16 border-b">{header}</header>
            <div className="flex flex-1">
                <aside className="w-64 border-r p-4">{sidebar}</aside>
                <main className="flex-1 p-8">{children}</main>
            </div>
        </div>
    );
}

// Usage
<PageLayout
    header={<NavBar user={user} />}
    sidebar={<CourseSidebar chapters={course.chapters} />}
>
    <LessonContent lesson={lesson} />
</PageLayout>

Render Props

A function prop that returns JSX — passes data from child to parent for rendering:

interface ListProps<T> {
    items: T[];
    renderItem: (item: T, index: number) => React.ReactNode;
    renderEmpty?: () => React.ReactNode;
}

function List<T>({ items, renderItem, renderEmpty }: ListProps<T>) {
    if (items.length === 0) {
        return renderEmpty ? <>{renderEmpty()}</> : <p>No items</p>;
    }
    
    return (
        <ul className="divide-y">
            {items.map((item, index) => (
                <li key={index} className="py-3">
                    {renderItem(item, index)}
                </li>
            ))}
        </ul>
    );
}

// Generic, reusable for any data type
<List
    items={courses}
    renderItem={(course) => (
        <div className="flex items-center gap-3">
            <img src={course.image} className="w-12 h-12 rounded" alt=""/>
            <div>
                <p className="font-medium">{course.title}</p>
                <p className="text-sm text-gray-500">${course.price}</p>
            </div>
        </div>
    )}
    renderEmpty={() => <p className="text-center py-8 text-gray-500">No courses found.</p>}
/>

Forwarding Props with Spread

For wrapper components that should accept all native element props:

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
    label: string;
    error?: string;
}

function FormInput({ label, error, className = "", ...props }: InputProps) {
    return (
        <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
                {label}
            </label>
            <input
                className={`w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 ${error ? "border-red-400" : "border-gray-300"} ${className}`}
                {...props}   // passes type, placeholder, disabled, value, onChange, etc.
            />
            {error && <p className="mt-1 text-sm text-red-600">{error}</p>}
        </div>
    );
}

// Works with all native input attributes
<FormInput
    label="Email"
    type="email"
    placeholder="alice@example.com"
    value={email}
    onChange={e => setEmail(e.target.value)}
    error={errors.email}
    disabled={loading}
    autoComplete="email"
/>

TypeScript Props Patterns

// Union types for variants
type ButtonVariant = "primary" | "secondary" | "danger" | "ghost";
type ButtonSize = "sm" | "md" | "lg";

interface ButtonProps {
    variant?: ButtonVariant;
    size?: ButtonSize;
    children: React.ReactNode;
    onClick?: () => void;
    disabled?: boolean;
}

// Discriminated union — different props for different uses
type AlertProps =
    | { type: "success"; message: string }
    | { type: "error"; message: string; onRetry?: () => void }
    | { type: "warning"; message: string; onDismiss?: () => void };

function Alert(props: AlertProps) {
    if (props.type === "error" && props.onRetry) {
        return (
            <div className="bg-red-50 border border-red-200 rounded p-4">
                <p>{props.message}</p>
                <button onClick={props.onRetry}>Try again</button>
            </div>
        );
    }
    // ...
}

// Polymorphic component — renders as different HTML elements
interface TextProps {
    as?: "h1" | "h2" | "h3" | "p" | "span";
    children: React.ReactNode;
    className?: string;
}

function Text({ as: Tag = "p", children, className }: TextProps) {
    return <Tag className={className}>{children}</Tag>;
}

<Text as="h1" className="text-4xl font-bold">Hero Title</Text>
<Text as="p" className="text-gray-600">Body text</Text>

Key Rules

  1. Props flow down — only parent-to-child, never child-to-parent directly
  2. Callbacks flow up — pass a function down so the child can call it and communicate back
  3. Never modify props — they're read-only; use state for things that change
  4. Keep prop interfaces minimal — only pass what the component actually needs

Next lesson: Event handling and synthetic events — making components respond to user interactions.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!