Component Composition and Context
Learn advanced component patterns including composition, context API, and avoiding prop drilling.
Topics Covered:
Prerequisites:
- Side Effects and useEffect Hook
Video Tutorial
Overview
As React applications grow, passing props through many component levels becomes tedious and hard to maintain. Component composition and the Context API provide elegant solutions to these problems. This tutorial covers composition patterns (children, render props, compound components), the Context API for sharing data across the component tree, when to use each pattern, and how to avoid common pitfalls. You'll learn to build flexible, maintainable component architectures.
Lesson 1: Understanding Prop Drilling
Prop drilling occurs when you pass props through multiple component levels just to reach a deeply nested component. What is Prop Drilling? • Passing props through components that don't use them • Props 'drill down' through the component tree • Makes components less reusable • Harder to maintain and refactor Problems with Prop Drilling: • Components become tightly coupled • Hard to refactor • Unnecessary re-renders • Components receive props they don't need • Makes code harder to understand When Prop Drilling Becomes a Problem: • Props passed through 3+ levels • Many props being passed down • Components receiving props they don't use • Hard to understand data flow
// ❌ BAD: Prop drilling example
function App() {
const user = { name: 'Alice', role: 'admin' };
const theme = 'dark';
const language = 'en';
return (
<Layout user={user} theme={theme} language={language}>
<Header user={user} theme={theme} />
<MainContent user={user} language={language}>
<Sidebar user={user} theme={theme} />
<Content user={user} language={language}>
<UserProfile user={user} /> {/* Finally uses user! */}
</Content>
</MainContent>
</Layout>
);
}
// Layout doesn't use user, theme, or language
function Layout({ children, user, theme, language }) {
return <div className={theme}>{children}</div>;
}
// Header doesn't use language
function Header({ user, theme }) {
return <header className={theme}>Welcome, {user.name}</header>;
}
// MainContent doesn't use theme
function MainContent({ children, user, language }) {
return <main>{children}</main>;
}
// Sidebar doesn't use language
function Sidebar({ user, theme }) {
return <aside className={theme}>Sidebar</aside>;
}
// Content doesn't use theme
function Content({ children, user, language }) {
return <div>{children}</div>;
}
// Only UserProfile actually uses user
function UserProfile({ user }) {
return <div>{user.name} - {user.role}</div>;
}
// Problems:
// - Layout, MainContent, Content receive props they don't use
// - Hard to refactor
// - Tight coupling
// - Unnecessary re-renders
// ✅ GOOD: Using Context (we'll learn this later)
// ✅ GOOD: Using Composition (we'll learn this now)Prop drilling makes components receive props they don't need, creating tight coupling and maintenance issues. Composition and Context solve this problem.
Lesson 2: Component Composition Basics
Composition is building complex components from simpler ones. It's one of React's core principles. What is Composition? • Building complex UIs from simple components • Components contain other components • Using children prop • Flexible and reusable Benefits: • Reusability • Flexibility • Separation of concerns • Easier to test • Better maintainability Composition Patterns: • Children prop • Multiple children slots • Render props • Compound components
// Basic composition with children
function Card({ children }) {
return (
<div className="card">
{children}
</div>
);
}
function App() {
return (
<Card>
<h2>Title</h2>
<p>Content</p>
</Card>
);
}
// Multiple composition slots
function Layout({ header, sidebar, main, footer }) {
return (
<div className="layout">
<header>{header}</header>
<div className="body">
<aside>{sidebar}</aside>
<main>{main}</main>
</div>
<footer>{footer}</footer>
</div>
);
}
function App() {
return (
<Layout
header={<Header />}
sidebar={<Sidebar />}
main={<MainContent />}
footer={<Footer />}
/>
);
}
// Composition with multiple children
function Container({ children, className }) {
return (
<div className={`container ${className}`}>
{children}
</div>
);
}
function App() {
return (
<Container className="main">
<Header />
<Content />
<Footer />
</Container>
);
}
// Flexible composition
function Button({ children, variant, size, ...props }) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
{...props}
>
{children}
</button>
);
}
function App() {
return (
<Button variant="primary" size="lg">
<Icon name="save" />
Save Document
</Button>
);
}Composition lets you build flexible, reusable components. Use the children prop to compose components together, creating complex UIs from simple building blocks.
Lesson 3: Advanced Composition Patterns
Learn advanced composition patterns for building flexible component APIs. Advanced Patterns: • Multiple children slots • Render props • Compound components • Slot-based composition • Higher-order components When to Use Each: • Multiple slots: When you need named sections • Render props: When you need to share logic • Compound components: When components work together • Slots: For flexible layouts
// Pattern 1: Multiple named children
function Dialog({
title,
content,
footer
}: {
title: React.ReactNode;
content: React.ReactNode;
footer: React.ReactNode;
}) {
return (
<div className="dialog">
<div className="dialog-header">{title}</div>
<div className="dialog-body">{content}</div>
<div className="dialog-footer">{footer}</div>
</div>
);
}
function App() {
return (
<Dialog
title={<h2>Confirm Action</h2>}
content={<p>Are you sure?</p>}
footer={
<>
<Button>Cancel</Button>
<Button variant="primary">Confirm</Button>
</>
}
/>
);
}
// Pattern 2: Render props
function DataFetcher({
url,
children
}: {
url: string;
children: (data: any, loading: boolean, error: any) => React.ReactNode;
}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return <>{children(data, loading, error)}</>;
}
function App() {
return (
<DataFetcher url="/api/users">
{(data, loading, error) => {
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <UserList users={data} />;
}}
</DataFetcher>
);
}
// Pattern 3: Compound components
const CardContext = createContext<{ variant?: string }>({});
function Card({ children, variant }: { children: React.ReactNode; variant?: string }) {
return (
<CardContext.Provider value={{ variant }}>
<div className={`card card-${variant}`}>
{children}
</div>
</CardContext.Provider>
);
}
Card.Header = function CardHeader({ children }: { children: React.ReactNode }) {
const { variant } = useContext(CardContext);
return <div className={`card-header ${variant}`}>{children}</div>;
};
Card.Body = function CardBody({ children }: { children: React.ReactNode }) {
return <div className="card-body">{children}</div>;
};
Card.Footer = function CardFooter({ children }: { children: React.ReactNode }) {
return <div className="card-footer">{children}</div>;
};
// Usage
function App() {
return (
<Card variant="outlined">
<Card.Header>Card Title</Card.Header>
<Card.Body>Card content goes here</Card.Body>
<Card.Footer>
<Button>Action</Button>
</Card.Footer>
</Card>
);
}
// Pattern 4: Slot-based composition
function PageLayout({
slots
}: {
slots: {
header?: React.ReactNode;
sidebar?: React.ReactNode;
main?: React.ReactNode;
footer?: React.ReactNode;
};
}) {
return (
<div className="page-layout">
{slots.header && <header>{slots.header}</header>}
<div className="page-body">
{slots.sidebar && <aside>{slots.sidebar}</aside>}
{slots.main && <main>{slots.main}</main>}
</div>
{slots.footer && <footer>{slots.footer}</footer>}
</div>
);
}
function App() {
return (
<PageLayout
slots={{
header: <Header />,
sidebar: <Sidebar />,
main: <MainContent />,
footer: <Footer />,
}}
/>
);
}
// Pattern 5: Flexible children with type checking
function Tabs({ children }: { children: React.ReactNode }) {
const [activeTab, setActiveTab] = useState(0);
const tabs = React.Children.toArray(children).filter(
child => React.isValidElement(child) && child.type === Tab
);
return (
<div className="tabs">
<div className="tab-list">
{tabs.map((tab, index) => (
<button
key={index}
onClick={() => setActiveTab(index)}
className={activeTab === index ? 'active' : ''}
>
{React.isValidElement(tab) && tab.props.label}
</button>
))}
</div>
<div className="tab-content">
{tabs[activeTab]}
</div>
</div>
);
}
function Tab({ label, children }: { label: string; children: React.ReactNode }) {
return <div>{children}</div>;
}
function App() {
return (
<Tabs>
<Tab label="Home">Home content</Tab>
<Tab label="About">About content</Tab>
<Tab label="Contact">Contact content</Tab>
</Tabs>
);
}Advanced composition patterns include multiple slots, render props, compound components, and slot-based composition. Each pattern solves different problems and provides flexibility.
Lesson 4: Introduction to Context API
Context API solves prop drilling by providing a way to share data across the component tree. What is Context? • Provides a way to pass data through component tree • Avoids prop drilling • Shares data with many components • Works with Provider/Consumer pattern When to Use Context: • Theme data (light/dark mode) • User authentication • Language/locale • Shared configuration • Data that many components need When NOT to Use Context: • Simple prop passing (1-2 levels) • Frequently changing data (causes re-renders) • Component-specific data (use props) • Global state (consider Redux/Zustand)
// Step 1: Create context
import { createContext } from 'react';
const ThemeContext = createContext<'light' | 'dark'>('light');
// Step 2: Create Provider component
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}
// Step 3: Use context in components
function Button() {
const theme = useContext(ThemeContext);
return (
<button className={`btn btn-${theme}`}>
Click me
</button>
);
}
// Step 4: Wrap app with Provider
function App() {
return (
<ThemeProvider>
<Header />
<MainContent />
<Footer />
</ThemeProvider>
);
}
// Complete example with value and setter
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook for easier access
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// Usage
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Current theme: {theme}
</button>
);
}
function ThemedButton() {
const { theme } = useTheme();
return (
<button className={`btn btn-${theme}`}>
Themed Button
</button>
);
}Context API solves prop drilling by providing a way to share data. Create context, provide it with a Provider, and consume it with useContext. Always create custom hooks for better developer experience.
Lesson 5: Creating and Using Context
Learn the complete process of creating and using Context in your applications. Context Creation Steps: 1. Create context with createContext 2. Create Provider component 3. Wrap app/components with Provider 4. Use useContext to access value 5. Create custom hook (optional but recommended) Best Practices: • Always provide default values • Create custom hooks for context access • Split contexts by concern • Use TypeScript for type safety • Handle missing Provider errors
// Complete context setup example
// Step 1: Define context type
interface UserContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
loading: boolean;
}
// Step 2: Create context
const UserContext = createContext<UserContextType | undefined>(undefined);
// Step 3: Create Provider
function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(false);
const login = async (email: string, password: string) => {
setLoading(true);
try {
const userData = await authenticate(email, password);
setUser(userData);
} catch (error) {
throw error;
} finally {
setLoading(false);
}
};
const logout = () => {
setUser(null);
};
const value: UserContextType = {
user,
login,
logout,
loading,
};
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}
// Step 4: Create custom hook
function useUser() {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('useUser must be used within UserProvider');
}
return context;
}
// Step 5: Use in components
function LoginButton() {
const { login, loading } = useUser();
const handleLogin = async () => {
try {
await login('user@example.com', 'password');
} catch (error) {
console.error('Login failed:', error);
}
};
return (
<button onClick={handleLogin} disabled={loading}>
{loading ? 'Logging in...' : 'Login'}
</button>
);
}
function UserProfile() {
const { user, logout } = useUser();
if (!user) {
return <div>Please log in</div>;
}
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
// Step 6: Wrap app
function App() {
return (
<UserProvider>
<Header />
<MainContent />
</UserProvider>
);
}
// Multiple contexts
function App() {
return (
<ThemeProvider>
<UserProvider>
<LanguageProvider>
<Header />
<MainContent />
</LanguageProvider>
</UserProvider>
</ThemeProvider>
);
}
// Using multiple contexts
function Header() {
const { theme } = useTheme();
const { user } = useUser();
const { language } = useLanguage();
return (
<header className={theme}>
<div>{language === 'en' ? 'Welcome' : 'Bienvenue'}</div>
{user && <UserMenu user={user} />}
</header>
);
}Creating context involves defining types, creating the context, building a Provider component, creating a custom hook, and using it in components. Always handle the case where context might be undefined.
Lesson 6: Context Performance Considerations
Context can cause performance issues if not used carefully. Learn how to optimize. Performance Issues: • Context value changes cause all consumers to re-render • Large context objects cause unnecessary re-renders • Frequent context updates impact performance Optimization Strategies: • Split contexts by update frequency • Memoize context values • Use multiple contexts • Memoize consumers with React.memo • Only put frequently changing data in separate context
// ❌ BAD: Single context with everything
interface AppContextType {
user: User;
theme: string;
language: string;
notifications: Notification[];
// All in one context - any change re-renders everything!
}
// ✅ GOOD: Split contexts by concern
const UserContext = createContext<User | null>(null);
const ThemeContext = createContext<string>('light');
const LanguageContext = createContext<string>('en');
const NotificationsContext = createContext<Notification[]>([]);
// ❌ BAD: New object every render
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// New object every render - causes re-renders!
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// ✅ GOOD: Memoize context value
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(
() => ({ theme, setTheme }),
[theme] // Only recreate when theme changes
);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// ✅ BETTER: Split value and setter
const ThemeContext = createContext<string>('light');
const ThemeUpdateContext = createContext<() => void>(() => {});
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// Theme value context - only changes when theme changes
// Update context - stable function reference
return (
<ThemeContext.Provider value={theme}>
<ThemeUpdateContext.Provider value={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
{children}
</ThemeUpdateContext.Provider>
</ThemeContext.Provider>
);
}
function useTheme() {
return useContext(ThemeContext);
}
function useThemeUpdate() {
return useContext(ThemeUpdateContext);
}
// Components that only read theme don't re-render when toggle is called
function ThemedComponent() {
const theme = useTheme(); // Only re-renders when theme changes
return <div className={theme}>Content</div>;
}
// Component that only toggles doesn't re-render when theme changes
function ThemeToggle() {
const toggle = useThemeUpdate(); // Stable reference, no re-renders
return <button onClick={toggle}>Toggle</button>;
}
// Memoize consumers
const ExpensiveComponent = React.memo(function ExpensiveComponent() {
const theme = useTheme();
return <div className={theme}>Expensive content</div>;
});
// Context with stable references
function UserProvider({ children }) {
const [user, setUser] = useState<User | null>(null);
// Memoize functions
const login = useCallback(async (email: string, password: string) => {
const userData = await authenticate(email, password);
setUser(userData);
}, []);
const logout = useCallback(() => {
setUser(null);
}, []);
const value = useMemo(
() => ({ user, login, logout }),
[user, login, logout]
);
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}Optimize context performance by splitting contexts, memoizing values, separating read/write contexts, and using React.memo for expensive consumers. Avoid creating new objects in Provider values.
Lesson 7: Real-World Context Examples
See how Context is used in real applications. Common Use Cases: • Theme management • User authentication • Language/locale • Shopping cart • Modal/dialog state • Form state • Feature flags
// Example 1: Theme context (complete)
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
// Load from localStorage
const saved = localStorage.getItem('theme');
return (saved as 'light' | 'dark') || 'light';
});
useEffect(() => {
// Save to localStorage
localStorage.setItem('theme', theme);
// Apply to document
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
const toggleTheme = useCallback(() => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
}, []);
const value = useMemo(
() => ({ theme, toggleTheme }),
[theme, toggleTheme]
);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used in ThemeProvider');
return context;
}
// Example 2: Authentication context
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
loading: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check for existing session
checkSession().then(setUser).finally(() => setLoading(false));
}, []);
const login = async (email: string, password: string) => {
setLoading(true);
try {
const userData = await authenticate(email, password);
setUser(userData);
} finally {
setLoading(false);
}
};
const logout = () => {
setUser(null);
clearSession();
};
const value = useMemo(
() => ({ user, login, logout, loading }),
[user, loading]
);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// Example 3: Shopping cart context
interface CartContextType {
items: CartItem[];
addItem: (product: Product) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
total: number;
itemCount: number;
}
const CartContext = createContext<CartContextType | undefined>(undefined);
function CartProvider({ children }: { children: React.ReactNode }) {
const [items, setItems] = useState<CartItem[]>([]);
const addItem = useCallback((product: Product) => {
setItems(prev => {
const existing = prev.find(item => item.product.id === product.id);
if (existing) {
return prev.map(item =>
item.product.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prev, { product, quantity: 1 }];
});
}, []);
const removeItem = useCallback((id: string) => {
setItems(prev => prev.filter(item => item.product.id !== id));
}, []);
const updateQuantity = useCallback((id: string, quantity: number) => {
setItems(prev =>
prev.map(item =>
item.product.id === id ? { ...item, quantity } : item
)
);
}, []);
const total = useMemo(
() => items.reduce((sum, item) => sum + item.product.price * item.quantity, 0),
[items]
);
const itemCount = useMemo(
() => items.reduce((sum, item) => sum + item.quantity, 0),
[items]
);
const value = useMemo(
() => ({ items, addItem, removeItem, updateQuantity, total, itemCount }),
[items, addItem, removeItem, updateQuantity, total, itemCount]
);
return (
<CartContext.Provider value={value}>
{children}
</CartContext.Provider>
);
}
// Example 4: Modal context
interface ModalContextType {
openModal: (content: React.ReactNode) => void;
closeModal: () => void;
isOpen: boolean;
}
const ModalContext = createContext<ModalContextType | undefined>(undefined);
function ModalProvider({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
const [content, setContent] = useState<React.ReactNode>(null);
const openModal = useCallback((modalContent: React.ReactNode) => {
setContent(modalContent);
setIsOpen(true);
}, []);
const closeModal = useCallback(() => {
setIsOpen(false);
setContent(null);
}, []);
const value = useMemo(
() => ({ openModal, closeModal, isOpen }),
[openModal, closeModal, isOpen]
);
return (
<ModalContext.Provider value={value}>
{children}
{isOpen && (
<Modal onClose={closeModal}>
{content}
</Modal>
)}
</ModalContext.Provider>
);
}Real-world context examples include theme management, authentication, shopping carts, and modals. Each demonstrates proper context setup with memoization and custom hooks.
Lesson 8: When to Use Composition vs Context
Understanding when to use composition versus context is crucial for good architecture. Use Composition When: • Building flexible component APIs • Creating reusable UI components • Components need to be customizable • You want to avoid prop drilling for UI structure • Building compound components Use Context When: • Sharing data across many components • Avoiding prop drilling for data • Theme, auth, language preferences • Data that doesn't change frequently • Configuration that many components need Use Both: • Context for data • Composition for structure • They complement each other
// ✅ Use Composition for structure
function Card({ children, variant }) {
return <div className={`card ${variant}`}>{children}</div>;
}
function App() {
return (
<Card variant="outlined">
<h2>Title</h2>
<p>Content</p>
</Card>
);
}
// ✅ Use Context for data
function App() {
return (
<ThemeProvider>
<UserProvider>
<Card variant="outlined">
<CardHeader>
<ThemedTitle /> {/* Uses theme from context */}
</CardHeader>
<CardBody>
<UserProfile /> {/* Uses user from context */}
</CardBody>
</Card>
</UserProvider>
</ThemeProvider>
);
}
// ✅ Combine both
function Dashboard() {
return (
<ThemeProvider>
<Layout>
<Header /> {/* Uses theme from context */}
<Sidebar>
<UserMenu /> {/* Uses user from context */}
</Sidebar>
<Main>
<Content /> {/* Uses both theme and user */}
</Main>
</Layout>
</ThemeProvider>
);
}
// ❌ Don't use Context for structure
// Bad: Using context to pass layout components
const LayoutContext = createContext({ header: null, footer: null });
// ✅ Good: Use composition
function Layout({ header, footer, children }) {
return (
<div>
{header}
{children}
{footer}
</div>
);
}
// ❌ Don't use Composition for deeply nested data
// Bad: Passing user through 10 components
function App() {
const user = { name: 'Alice' };
return (
<Layout user={user}>
<Header user={user}>
<Nav user={user}>
<UserMenu user={user} />
</Nav>
</Header>
</Layout>
);
}
// ✅ Good: Use context
function App() {
return (
<UserProvider>
<Layout>
<Header>
<Nav>
<UserMenu /> {/* Gets user from context */}
</Nav>
</Header>
</Layout>
</UserProvider>
);
}Use composition for structure and flexibility, context for sharing data. They work together - use context for data (theme, user) and composition for UI structure (layouts, cards).
Lesson 9: Common Mistakes and Best Practices
Avoid common mistakes when using composition and context. Common Mistakes: • Using context for everything • Not memoizing context values • Creating contexts that change too frequently • Not splitting contexts • Forgetting to handle missing Provider • Over-engineering simple prop passing Best Practices: • Split contexts by concern • Memoize context values • Create custom hooks • Handle missing Provider errors • Use composition for structure • Use context for data • Don't overuse context
// ❌ MISTAKE 1: Using context for everything
const EverythingContext = createContext({
user: null,
theme: 'light',
cart: [],
notifications: [],
settings: {},
// Everything in one context!
});
// ✅ FIX: Split by concern
const UserContext = createContext(null);
const ThemeContext = createContext('light');
const CartContext = createContext([]);
// ❌ MISTAKE 2: New object every render
function Provider({ children }) {
const [state, setState] = useState({});
return (
<Context.Provider value={{ state, setState }}>
{/* New object every render! */}
</Context.Provider>
);
}
// ✅ FIX: Memoize value
function Provider({ children }) {
const [state, setState] = useState({});
const value = useMemo(
() => ({ state, setState }),
[state]
);
return (
<Context.Provider value={value}>
{children}
</Context.Provider>
);
}
// ❌ MISTAKE 3: Not handling missing Provider
function Component() {
const value = useContext(MyContext);
// Crashes if Provider is missing!
return <div>{value.data}</div>;
}
// ✅ FIX: Check for undefined
function useMyContext() {
const context = useContext(MyContext);
if (!context) {
throw new Error('useMyContext must be used within MyProvider');
}
return context;
}
// ❌ MISTAKE 4: Using context for simple prop passing
function App() {
return (
<NameProvider>
<Component /> {/* Only 1 level deep! */}
</NameProvider>
);
}
// ✅ FIX: Just use props
function App() {
const name = 'Alice';
return <Component name={name} />;
}
// ❌ MISTAKE 5: Context with frequently changing data
function CounterProvider({ children }) {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => setCount(c => c + 1), 100);
return () => clearInterval(timer);
}, []);
// Count changes 10 times per second!
// All consumers re-render constantly!
return (
<CounterContext.Provider value={count}>
{children}
</CounterContext.Provider>
);
}
// ✅ FIX: Don't use context for frequently changing data
// Use props or state management library instead
// Best practices checklist:
// ✅ Split contexts by concern
// ✅ Memoize context values
// ✅ Create custom hooks
// ✅ Handle missing Provider
// ✅ Use composition for structure
// ✅ Use context for shared data
// ✅ Don't overuse contextCommon mistakes include using context for everything, not memoizing values, and using context for frequently changing data. Follow best practices: split contexts, memoize values, create custom hooks, and use composition for structure.
Conclusion
Component composition and Context API are powerful tools for building maintainable React applications. Use composition for flexible UI structure and Context for sharing data across the component tree. Remember: composition for structure, context for data. Split contexts by concern, memoize values, and always create custom hooks for better developer experience.