đ§Š 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. If TypeScript is the safety net you met in Module 1, components are the bricks you'll actually build with. Once you can think in components, the rest of React clicks into place â so let's start here and go slowly.
Here's the mental model in one sentence: a React component is just a function that takes some data and returns a description of what the screen should look like. That's it. No magic, no classes, no ceremony â a function in, some UI out. Everything else in this lesson is a refinement of that single idea.
đ Definition
Component: A self-contained, reusable piece of UI that encapsulates its structure (markup), appearance (styles), and behavior (logic). A component accepts inputs called props and returns React elements describing what should appear on screen. Because it's a plain function, the same inputs always produce the same output â which makes components easy to reason about and test.
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)
In React 19, a component is simply a function whose name starts with a capital letter and that returns JSX. There are no classes to learn and no boilerplate â if you can write a function, you can write a component:
// Basic function component (declaration)
function Welcome() {
return <h1>Hello, World!</h1>;
}
// Arrow function component (expression) â identical result
const Goodbye = () => {
return <h1>Goodbye, World!</h1>;
};
Both styles compile to the same thing. Use whichever your team prefers â many codebases use function declarations for top-level components (they hoist and show up nicely in stack traces) and arrow functions for small inline helpers. The capital letter matters: React uses it to tell your component (<Welcome />) apart from a built-in HTML tag (<h1>).
â ī¸ Skip React.FC â Just Write a Plain Function
You'll still spot React.FC (short for "Function Component") in older tutorials and code. The React team now steers you away from it, and in React 19 there's no reason to reach for it. A plainly-typed function is simpler, more flexible, and reads better:
// â
Recommended: a plain function with typed props
interface GreetingProps {
name: string;
}
function Greeting({ name }: GreetingProps) {
return <h1>Hello, {name}!</h1>;
}
// â Avoid: React.FC (verbose, and historically forced an
// implicit children prop your component may not even accept)
const GreetingFC: React.FC<GreetingProps> = ({ name }) => {
return <h1>Hello, {name}!</h1>;
};
The rule of thumb for this whole lesson: write a plain function and describe its props with an interface or type. We'll do exactly that from here on.
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
A component that always renders the same thing isn't very useful â imagine a Greeting that could only ever say "Hello, World". Props (short for "properties") are how you feed data into a component so it can render something different each time. If a component is a function, props are its arguments. That one analogy will carry you a long way.
đ Definition
Props: Arguments passed to a component, just like parameters passed to a function. They let a parent component hand data and behavior down to a child. Props are read-only and flow in one direction â from parent to child â which is what keeps a React app predictable.
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>
);
}
Notice the direction of the arrows in the diagram below. Data travels down as props; when a child needs to talk back, the parent hands it a function and the child calls it. This "data down, events up" shape is the backbone of every React app you'll ever build.
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 |
See What the Compiler Allows
Here's the practical upshot of "props are read-only." Click each line to see whether TypeScript (and React's mental model) would accept it. Notice that reading a prop is always fine, reassigning one is not, and mutable data belongs in state.
function Badge({ label }: { label: string }) { const [count, setCount] = useState(0); ... }
đ Click a line to see whether it respects React's rules.
đˇ 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>
Passing a ref â Much Simpler in React 19
Sometimes a parent needs a direct handle on a DOM node inside your component â to focus an input, measure a box, or scroll it into view. For years this meant wrapping your component in forwardRef, which was clunky and easy to forget. In React 19 that's gone: ref is just a normal prop. You type it and destructure it like any other:
interface TextInputProps {
label: string;
ref?: React.Ref<HTMLInputElement>; // just another prop now
}
// No forwardRef needed in React 19 đ
function TextInput({ label, ref }: TextInputProps) {
return (
<label>
{label}
<input ref={ref} type="text" />
</label>
);
}
// Parent passes a ref exactly like any other prop
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return <TextInput label="Name" ref={inputRef} />;
}
đĄ Modernizing older code
If you're reading a codebase written before React 19, you'll still see const X = forwardRef((props, ref) => ...). It keeps working, but for new components you no longer need it â declare ref in your props interface and treat it like any other value. One less API to memorize!
đļ 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 React passes automatically, holding whatever content sits between a component's opening and closing tags. It's what lets your own components wrap content just like a native <div> does â the key to real composition.
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
The diagram below shows the whole trick at a glance: whatever you nest between <Card> and </Card> is quietly bundled up and handed to your component as props.children. Your component decides where to render it.
children prop â text, elements, other components, or any mix of them.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.
// Give `user` a real type instead of `any` â never reach for `any` here.
interface User {
name: string;
avatar: string;
bio: string;
}
function UserCard({ user }: { user: User }) {
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>;
}
Composition is easiest to see with a picture. On the left is the code â a UserCard assembled from four small, independently-reusable components. On the right is what the user actually sees. Each piece could be lifted out and used somewhere else entirely; that's the payoff.
UserCard composed from four smaller components. Composition â not inheritance â is how React builds complex UIs out of simple parts.đ¨ 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
- Skip
React.FC- In React 19, plainly-typed function components are the recommended default - 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! đ