Props and Component Composition
Understanding Props
Props (properties) are the way data flows from parent to child components in React. They are read-only and help make components reusable.
Props Flow
// Parent component
function App() {
const user = {
name: 'Alice',
email: 'alice@example.com',
role: 'Developer',
};
return <UserProfile user={user} isActive={true} />;
}
// Child component receives props
function UserProfile({ user, isActive }) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>Role: {user.role}</p>
{isActive && <span>🟢 Active</span>}
</div>
);
}
Props are Read-Only (Immutable)
// ❌ NEVER modify props
function Component({ count }) {
count = count + 1; // This is wrong!
return <div>{count}</div>;
}
// ✅ Props should be treated as immutable
function Component({ count }) {
const newCount = count + 1; // Create new value
return <div>{newCount}</div>;
}
TypeScript Props
Interface vs Type
// Interface (preferred for props)
interface ButtonProps {
text: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
}
// Type alias (also valid)
type ButtonProps = {
text: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
};
const Button = ({
text,
onClick,
variant = 'primary',
disabled = false,
}: ButtonProps) => (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{text}
</button>
);
Complex Prop Types
interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
interface UserListProps {
users: User[];
onUserClick: (userId: string) => void;
renderHeader?: () => React.ReactNode;
emptyMessage?: string;
className?: string;
}
const UserList = ({
users,
onUserClick,
renderHeader,
emptyMessage = 'No users found',
className = '',
}: UserListProps) => {
if (users.length === 0) {
return <p>{emptyMessage}</p>;
}
return (
<div className={className}>
{renderHeader && renderHeader()}
<ul>
{users.map(user => (
<li key={user.id} onClick={() => onUserClick(user.id)}>
{user.name}
</li>
))}
</ul>
</div>
);
};
Component Composition Patterns
Container and Presentational Pattern
// Presentational Component (Dumb/Pure)
const UserCard = ({ name, email, avatar, onEdit }) => (
<div className='user-card'>
<img src={avatar} alt={name} />
<h3>{name}</h3>
<p>{email}</p>
<button onClick={onEdit}>Edit</button>
</div>
);
// Container Component (Smart)
const UserCardContainer = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser(userId).then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
const handleEdit = () => {
// Handle edit logic
};
if (loading) return <Spinner />;
if (!user) return <ErrorMessage />;
return (
<UserCard
name={user.name}
email={user.email}
avatar={user.avatar}
onEdit={handleEdit}
/>
);
};
Composition with Children
// Layout components using children
const Card = ({ children, title, footer }) => (
<div className='card'>
{title && <div className='card-header'>{title}</div>}
<div className='card-body'>{children}</div>
{footer && <div className='card-footer'>{footer}</div>}
</div>
);
const Page = ({ children, sidebar }) => (
<div className='page-layout'>
<aside className='sidebar'>{sidebar}</aside>
<main className='content'>{children}</main>
</div>
);
// Usage
function App() {
return (
<Page sidebar={<Navigation />}>
<Card title='User Profile' footer={<button>Save</button>}>
<UserForm />
</Card>
</Page>
);
}
Render Props Pattern
// Component with render prop
const DataFetcher = ({ url, render }) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [url]);
return render({ data, loading, error });
};
// Usage
function App() {
return (
<DataFetcher
url='/api/users'
render={({ data, loading, error }) => {
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <UserList users={data} />;
}}
/>
);
}
Compound Components
// Tab component system
const TabContext = createContext();
const Tabs = ({ children, defaultTab }) => {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabContext.Provider value={{ activeTab, setActiveTab }}>
<div className='tabs'>{children}</div>
</TabContext.Provider>
);
};
const TabList = ({ children }) => <div className='tab-list'>{children}</div>;
const Tab = ({ id, children }) => {
const { activeTab, setActiveTab } = useContext(TabContext);
return (
<button
className={activeTab === id ? 'active' : ''}
onClick={() => setActiveTab(id)}
>
{children}
</button>
);
};
const TabPanel = ({ id, children }) => {
const { activeTab } = useContext(TabContext);
return activeTab === id ? <div className='tab-panel'>{children}</div> : null;
};
// Export as compound component
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
// Usage
function App() {
return (
<Tabs defaultTab='profile'>
<Tabs.List>
<Tabs.Tab id='profile'>Profile</Tabs.Tab>
<Tabs.Tab id='settings'>Settings</Tabs.Tab>
<Tabs.Tab id='notifications'>Notifications</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id='profile'>
<ProfileContent />
</Tabs.Panel>
<Tabs.Panel id='settings'>
<SettingsContent />
</Tabs.Panel>
<Tabs.Panel id='notifications'>
<NotificationsContent />
</Tabs.Panel>
</Tabs>
);
}
Props Spreading and Rest Parameters
Spreading Props
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}
const Input = ({ label, error, ...inputProps }: InputProps) => (
<div className='form-field'>
<label>{label}</label>
<input {...inputProps} className={error ? 'error' : ''} />
{error && <span className='error-message'>{error}</span>}
</div>
);
// Usage - all standard input props work
<Input
label='Email'
type='email'
placeholder='Enter email'
required
autoComplete='email'
error={emailError}
/>;
Polymorphic Components
type ButtonProps<T extends React.ElementType> = {
as?: T;
variant?: 'primary' | 'secondary';
children: React.ReactNode;
} & React.ComponentPropsWithoutRef\<T\>;
const Button = <T extends React.ElementType = 'button'>({
as,
variant = 'primary',
children,
...props
}: ButtonProps\<T\>) => {
const Component = as || 'button';
return (
<Component className={`btn btn-${variant}`} {...props}>
{children}
</Component>
);
};
// Usage
<Button onClick={handleClick}>Regular Button</Button>
<Button as="a" href="/profile">Link Button</Button>
<Button as={Link} to="/home">Router Link Button</Button>
Prop Drilling and Solutions
The Problem
// Prop drilling - passing props through many levels
function App() {
const [user, setUser] = useState({ name: 'Alice', theme: 'dark' });
return <Dashboard user={user} />;
}
function Dashboard({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <UserMenu user={user} />;
}
function UserMenu({ user }) {
return <UserProfile user={user} />;
}
function UserProfile({ user }) {
return <div>{user.name}</div>;
}
Solution 1: Context API
const UserContext = createContext();
function App() {
const [user, setUser] = useState({ name: 'Alice', theme: 'dark' });
return (
<UserContext.Provider value={{ user, setUser }}>
<Dashboard />
</UserContext.Provider>
);
}
function UserProfile() {
const { user } = useContext(UserContext);
return <div>{user.name}</div>;
}
Solution 2: Component Composition
function App() {
const [user, setUser] = useState({ name: 'Alice' });
return (
<Dashboard>
<Sidebar>
<UserProfile user={user} />
</Sidebar>
</Dashboard>
);
}
function Dashboard({ children }) {
return <div className='dashboard'>{children}</div>;
}
function Sidebar({ children }) {
return <aside className='sidebar'>{children}</aside>;
}
Default Props (Modern Approach)
// ❌ Old way (deprecated in function components)
Button.defaultProps = {
variant: 'primary',
disabled: false,
};
// ✅ Modern way - default parameters
const Button = ({ variant = 'primary', disabled = false, children }) => (
<button className={`btn btn-${variant}`} disabled={disabled}>
{children}
</button>
);
// ✅ TypeScript with defaults
interface ButtonProps {
variant?: 'primary' | 'secondary';
disabled?: boolean;
children: React.ReactNode;
}
const Button = ({
variant = 'primary',
disabled = false,
children,
}: ButtonProps) => (
<button className={`btn btn-${variant}`} disabled={disabled}>
{children}
</button>
);
Prop Validation Best Practices
- Use TypeScript - Compile-time type checking
- Provide Meaningful Defaults - Make components easier to use
- Keep Prop Interfaces Simple - Don't overcomplicate
- Document Complex Props - Use JSDoc comments
- Avoid Prop Mutation - Props are immutable
interface ComplexComponentProps {
/** Primary user data */
user: User;
/** Callback when user is updated */
onUserUpdate: (user: User) => void;
/** Optional custom renderer for user avatar */
renderAvatar?: (user: User) => React.ReactNode;
/** Display mode for the component */
mode?: 'compact' | 'expanded';
/** Additional CSS classes */
className?: string;
}
Practice Exercises
- Create a
Modalcompound component withModal.Header,Modal.Body, andModal.Footer - Build a
Formcomponent that spreads props to the underlying form element - Implement a
DataTablecomponent with render props for custom cell rendering - Create a polymorphic
Textcomponent that can render as different HTML elements
Next Steps
- Learn about Virtual DOM
- Explore Events and Forms