AiTechWorlds
AiTechWorlds
Props are the read-only inputs a parent component passes to a child, similar to the arguments a function receives.
Think of a vending machine. You press a button (the prop) and the machine dispenses the matching item — it never rewrites its own buttons or restocks itself based on what you press. A component works the same way: it reads its props and renders accordingly, but it can never reach back and change what the parent gave it. Master the composition 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 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>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 — preferred
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) {
// ...
}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>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.
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>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>}
/>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"
/>// 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>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