Learn advanced React patterns including compound components, render props, higher-order components, and custom hooks for building scalable applications.
Learn advanced React patterns including compound components, render props, higher-order components, and custom hooks for building scalable applications.
Learn compound components pattern for creating flexible and reusable component APIs
Content by: Harsh Patel
MERN Stack Developer
Compound components are a pattern where components are used together such that they share an implicit state that lets them communicate with each other. This pattern provides a flexible and intuitive API for complex components.
// Basic compound components example
import React, { createContext, useContext, useState } from 'react';
const TabsContext = createContext();
function Tabs({ children, defaultIndex = 0 }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
return (
<TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
<div className="tabs">
{children}
</div>
</TabsContext.Provider>
);
}
function TabList({ children }) {
return (
<div className="tab-list">
{children}
</div>
);
}
function Tab({ index, children }) {
const { activeIndex, setActiveIndex } = useContext(TabsContext);
const isActive = index === activeIndex;
return (
<button
className={`tab ${isActive ? 'active' : ''}`}
onClick={() => setActiveIndex(index)}
>
{children}
</button>
);
}
function TabPanels({ children }) {
return (
<div className="tab-panels">
{children}
</div>
);
}
function TabPanel({ index, children }) {
const { activeIndex } = useContext(TabsContext);
if (index !== activeIndex) return null;
return (
<div className="tab-panel">
{children}
</div>
);
}
// Usage
function App() {
return (
<Tabs defaultIndex={0}>
<TabList>
<Tab index={0}>Home</Tab>
<Tab index={1}>About</Tab>
<Tab index={2}>Contact</Tab>
</TabList>
<TabPanels>
<TabPanel index={0}>
<h2>Home Content</h2>
<p>Welcome to our website!</p>
</TabPanel>
<TabPanel index={1}>
<h2>About Content</h2>
<p>Learn more about us.</p>
</TabPanel>
<TabPanel index={2}>
<h2>Contact Content</h2>
<p>Get in touch with us.</p>
</TabPanel>
</TabPanels>
</Tabs>
);
}
// Advanced compound components with flexible API
import React, { createContext, useContext, useState, cloneElement, Children } from 'react';
const AccordionContext = createContext();
function Accordion({ children, multiple = false }) {
const [openItems, setOpenItems] = useState(new Set());
const toggleItem = (index) => {
if (multiple) {
const newOpenItems = new Set(openItems);
if (newOpenItems.has(index)) {
newOpenItems.delete(index);
} else {
newOpenItems.add(index);
}
setOpenItems(newOpenItems);
} else {
setOpenItems(new Set([openItems.has(index) ? null : index]));
}
};
const isOpen = (index) => openItems.has(index);
return (
<AccordionContext.Provider value={{ toggleItem, isOpen }}>
<div className="accordion">
{Children.map(children, (child, index) =>
cloneElement(child, { index })
)}
</div>
</AccordionContext.Provider>
);
}
function AccordionItem({ index, children }) {
const { toggleItem, isOpen } = useContext(AccordionContext);
const open = isOpen(index);
return (
<div className={`accordion-item ${open ? 'open' : ''}`}>
{Children.map(children, child =>
cloneElement(child, { index, open, toggleItem })
)}
</div>
);
}
function AccordionTrigger({ index, children, open, toggleItem }) {
return (
<button
className="accordion-trigger"
onClick={() => toggleItem(index)}
>
{children}
<span className={`arrow ${open ? 'up' : 'down'}`}>ā¼</span>
</button>
);
}
function AccordionContent({ open, children }) {
return (
<div className={`accordion-content ${open ? 'open' : ''}`}>
{children}
</div>
);
}
// Usage
function App() {
return (
<Accordion multiple>
<AccordionItem>
<AccordionTrigger>Section 1</AccordionTrigger>
<AccordionContent>
<p>Content for section 1</p>
</AccordionContent>
</AccordionItem>
<AccordionItem>
<AccordionTrigger>Section 2</AccordionTrigger>
<AccordionContent>
<p>Content for section 2</p>
</AccordionContent>
</AccordionItem>
</Accordion>
);
}
Test your understanding of this topic:
Master the render props pattern for sharing behavior between components
Content by: Dharmik Tank
MERN Stack Developer
Render props is a technique for sharing code between React components using a prop whose value is a function. This pattern allows components to share behavior while maintaining flexibility in how they render.
// Basic render props example
import React, { useState } from 'react';
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (event) => {
setPosition({
x: event.clientX,
y: event.clientY
});
};
return (
<div onMouseMove={handleMouseMove} style={{ height: '100vh' }}>
{render(position)}
</div>
);
}
// Usage
function App() {
return (
<MouseTracker
render={({ x, y }) => (
<div>
<h1>Mouse Position</h1>
<p>X: {x}, Y: {y}</p>
</div>
)}
/>
);
}
// Another example with data fetching
function DataFetcher({ url, render }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
React.useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return render({ data, loading, error });
}
// Usage
function UserList() {
return (
<DataFetcher
url="/api/users"
render={({ data, loading, error }) => {
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!data) return <div>No data</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}}
/>
);
}
// Advanced render props with multiple behaviors
import React, { useState, useEffect } from 'react';
function Form({ initialValues = {}, onSubmit, children }) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
if (touched[name]) {
// Clear error when user starts typing
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
const handleBlur = (name) => {
setTouched(prev => ({ ...prev, [name]: true }));
};
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(values);
};
const getFieldProps = (name) => ({
value: values[name] || '',
onChange: (e) => handleChange(name, e.target.value),
onBlur: () => handleBlur(name),
error: errors[name],
touched: touched[name]
});
return (
<form onSubmit={handleSubmit}>
{children({ getFieldProps, values, errors, touched })}
</form>
);
}
// Usage
function LoginForm() {
const handleSubmit = (values) => {
console.log('Form submitted:', values);
};
return (
<Form
initialValues={{ email: '', password: '' }}
onSubmit={handleSubmit}
>
{({ getFieldProps, errors, touched }) => (
<>
<div>
<input
type="email"
placeholder="Email"
{...getFieldProps('email')}
/>
{touched.email && errors.email && (
<span className="error">{errors.email}</span>
)}
</div>
<div>
<input
type="password"
placeholder="Password"
{...getFieldProps('password')}
/>
{touched.password && errors.password && (
<span className="error">{errors.password}</span>
)}
</div>
<button type="submit">Login</button>
</>
)}
</Form>
);
}
// Toggle component with render props
function Toggle({ children }) {
const [on, setOn] = useState(false);
const toggle = () => setOn(!on);
const reset = () => setOn(false);
return children({ on, toggle, reset });
}
// Usage
function ToggleExample() {
return (
<Toggle>
{({ on, toggle, reset }) => (
<div>
<button onClick={toggle}>
{on ? 'ON' : 'OFF'}
</button>
<button onClick={reset}>Reset</button>
<p>Toggle is {on ? 'ON' : 'OFF'}</p>
</div>
)}
</Toggle>
);
}
Test your understanding of this topic:
Learn Higher-Order Components for cross-cutting concerns and component composition
Content by: Hiren Bhut
MERN Stack Developer
Higher-Order Components (HOCs) are functions that take a component and return a new component with additional props or behavior. They are useful for sharing logic between components.
// Basic HOC example
import React from 'react';
// HOC for adding loading state
function withLoading(WrappedComponent) {
return function WithLoadingComponent(props) {
const [loading, setLoading] = React.useState(false);
const [data, setData] = React.useState(null);
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch(props.url);
const result = await response.json();
setData(result);
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoading(false);
}
};
React.useEffect(() => {
fetchData();
}, [props.url]);
if (loading) {
return <div>Loading...</div>;
}
return <WrappedComponent {...props} data={data} />;
};
}
// Component to be enhanced
function UserList({ data }) {
if (!data) return <div>No data available</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// Enhanced component
const UserListWithLoading = withLoading(UserList);
// Usage
function App() {
return <UserListWithLoading url="/api/users" />;
}
// HOC for authentication
function withAuth(WrappedComponent) {
return function WithAuthComponent(props) {
const [user, setUser] = React.useState(null);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
// Check if user is authenticated
const checkAuth = async () => {
try {
const token = localStorage.getItem('token');
if (token) {
const response = await fetch('/api/me', {
headers: { Authorization: `Bearer ${token}` }
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
}
}
} catch (error) {
console.error('Auth check failed:', error);
} finally {
setLoading(false);
}
};
checkAuth();
}, []);
if (loading) {
return <div>Checking authentication...</div>;
}
if (!user) {
return <div>Please log in to access this page</div>;
}
return <WrappedComponent {...props} user={user} />;
};
}
// Protected component
function Dashboard({ user }) {
return (
<div>
<h1>Welcome, {user.name}!</h1>
<p>This is your dashboard</p>
</div>
);
}
const ProtectedDashboard = withAuth(Dashboard);
// Advanced HOC with composition and configuration
import React from 'react';
// HOC for error boundaries
function withErrorBoundary(WrappedComponent) {
return class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return <WrappedComponent {...this.props} />;
}
};
}
// HOC for performance monitoring
function withPerformanceMonitoring(WrappedComponent, componentName) {
return function PerformanceMonitoredComponent(props) {
const startTime = React.useRef(Date.now());
React.useEffect(() => {
const endTime = Date.now();
const renderTime = endTime - startTime.current;
console.log(`${componentName} rendered in ${renderTime}ms`);
});
return <WrappedComponent {...props} />;
};
}
// HOC for data caching
function withCache(WrappedComponent, cacheKey) {
return function CachedComponent(props) {
const [cachedData, setCachedData] = React.useState(null);
React.useEffect(() => {
const cached = localStorage.getItem(cacheKey);
if (cached) {
try {
setCachedData(JSON.parse(cached));
} catch (error) {
console.error('Failed to parse cached data:', error);
}
}
}, []);
const updateCache = (data) => {
localStorage.setItem(cacheKey, JSON.stringify(data));
setCachedData(data);
};
return (
<WrappedComponent
{...props}
cachedData={cachedData}
updateCache={updateCache}
/>
);
};
}
// HOC composition
function compose(...hocs) {
return (Component) => {
return hocs.reduce((acc, hoc) => hoc(acc), Component);
};
}
// Usage with multiple HOCs
const EnhancedComponent = compose(
withErrorBoundary,
(Component) => withPerformanceMonitoring(Component, 'UserList'),
(Component) => withCache(Component, 'user-list-cache')
)(UserList);
// HOC with configuration
function withConfig(config) {
return function(WrappedComponent) {
return function ConfiguredComponent(props) {
return (
<WrappedComponent
{...props}
config={config}
/>
);
};
};
}
// Usage
const UserListWithConfig = withConfig({
pageSize: 10,
sortBy: 'name',
enableSearch: true
})(UserList);
Test your understanding of this topic:
Learn advanced custom hooks patterns for complex state management and side effects
Content by: Mayur Bhalgama
React.js Developer
// Advanced custom hooks for complex scenarios
import { useState, useEffect, useCallback, useRef } from 'react';
// Hook for managing form state with validation
function useForm(initialValues = {}, validationSchema = {}) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const validateField = useCallback((name, value) => {
const rules = validationSchema[name];
if (!rules) return '';
for (const rule of rules) {
if (rule.required && !value) {
return rule.message || `${name} is required`;
}
if (rule.minLength && value.length < rule.minLength) {
return rule.message || `${name} must be at least ${rule.minLength} characters`;
}
if (rule.pattern && !rule.pattern.test(value)) {
return rule.message || `${name} format is invalid`;
}
}
return '';
}, [validationSchema]);
const handleChange = useCallback((name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
if (touched[name]) {
const error = validateField(name, value);
setErrors(prev => ({ ...prev, [name]: error }));
}
}, [touched, validateField]);
const handleBlur = useCallback((name) => {
setTouched(prev => ({ ...prev, [name]: true }));
const error = validateField(name, values[name]);
setErrors(prev => ({ ...prev, [name]: error }));
}, [values, validateField]);
const handleSubmit = useCallback(async (onSubmit) => {
setIsSubmitting(true);
// Validate all fields
const newErrors = {};
Object.keys(validationSchema).forEach(name => {
const error = validateField(name, values[name]);
if (error) newErrors[name] = error;
});
setErrors(newErrors);
setTouched(Object.keys(validationSchema).reduce((acc, key) => ({ ...acc, [key]: true }), {}));
if (Object.keys(newErrors).length === 0) {
await onSubmit(values);
}
setIsSubmitting(false);
}, [values, validationSchema, validateField]);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
setIsSubmitting(false);
}, [initialValues]);
return {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
reset
};
}
// Hook for API calls with caching
function useApi(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const cacheRef = useRef(new Map());
const abortControllerRef = useRef(null);
const fetchData = useCallback(async () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
// Check cache first
if (options.cache && cacheRef.current.has(url)) {
const cached = cacheRef.current.get(url);
if (Date.now() - cached.timestamp < options.cacheTime) {
setData(cached.data);
setLoading(false);
return;
}
}
try {
setLoading(true);
setError(null);
const response = await fetch(url, {
...options,
signal: abortControllerRef.current.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
// Cache the result
if (options.cache) {
cacheRef.current.set(url, {
data: result,
timestamp: Date.now()
});
}
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
}, [url, options]);
useEffect(() => {
fetchData();
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, [fetchData]);
const refetch = useCallback(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch };
}
// Hook for managing local storage
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
const setValue = useCallback((value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
}, [key, storedValue]);
return [storedValue, setValue];
}
// Hook for managing window size
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowSize;
}
// Usage examples
function LoginForm() {
const validationSchema = {
email: [
{ required: true, message: 'Email is required' },
{ pattern: /^[^s@]+@[^s@]+.[^s@]+$/, message: 'Invalid email format' }
],
password: [
{ required: true, message: 'Password is required' },
{ minLength: 8, message: 'Password must be at least 8 characters' }
]
};
const { values, errors, touched, isSubmitting, handleChange, handleBlur, handleSubmit } = useForm({}, validationSchema);
const onSubmit = async (formData) => {
console.log('Form submitted:', formData);
// Handle form submission
};
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(onSubmit); }}>
<div>
<input
type="email"
value={values.email || ''}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={() => handleBlur('email')}
placeholder="Email"
/>
{touched.email && errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<input
type="password"
value={values.password || ''}
onChange={(e) => handleChange('password', e.target.value)}
onBlur={() => handleBlur('password')}
placeholder="Password"
/>
{touched.password && errors.password && <span className="error">{errors.password}</span>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Login'}
</button>
</form>
);
}
// Exercise: Build a Complete Data Management System
// Create a comprehensive data management system using advanced patterns
// Custom hook for managing data with CRUD operations
function useDataManager(initialData = []) {
const [data, setData] = useState(initialData);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [filters, setFilters] = useState({});
const [sortBy, setSortBy] = useState(null);
const [sortDirection, setSortDirection] = useState('asc');
// Filter and sort data
const processedData = useMemo(() => {
let result = [...data];
// Apply filters
Object.entries(filters).forEach(([key, value]) => {
if (value) {
result = result.filter(item =>
item[key].toLowerCase().includes(value.toLowerCase())
);
}
});
// Apply sorting
if (sortBy) {
result.sort((a, b) => {
const aValue = a[sortBy];
const bValue = b[sortBy];
if (sortDirection === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
}
return result;
}, [data, filters, sortBy, sortDirection]);
const addItem = useCallback((item) => {
setData(prev => [...prev, { ...item, id: Date.now() }]);
}, []);
const updateItem = useCallback((id, updates) => {
setData(prev => prev.map(item =>
item.id === id ? { ...item, ...updates } : item
));
}, []);
const deleteItem = useCallback((id) => {
setData(prev => prev.filter(item => item.id !== id));
}, []);
const setFilter = useCallback((key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
}, []);
const setSorting = useCallback((key) => {
if (sortBy === key) {
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');
} else {
setSortBy(key);
setSortDirection('asc');
}
}, [sortBy]);
return {
data: processedData,
loading,
error,
addItem,
updateItem,
deleteItem,
setFilter,
setSorting,
sortBy,
sortDirection,
filters
};
}
// Compound component for data table
const DataTableContext = createContext();
function DataTable({ children, data, onSort, sortBy, sortDirection }) {
return (
<DataTableContext.Provider value={{ data, onSort, sortBy, sortDirection }}>
<table className="data-table">
{children}
</table>
</DataTableContext.Provider>
);
}
function DataTableHeader({ children, sortKey }) {
const { onSort, sortBy, sortDirection } = useContext(DataTableContext);
return (
<th
onClick={() => onSort(sortKey)}
className={`sortable ${sortBy === sortKey ? 'sorted' : ''}`}
>
{children}
{sortBy === sortKey && (
<span className={`arrow ${sortDirection}`}>ā¼</span>
)}
</th>
);
}
function DataTableBody({ children }) {
const { data } = useContext(DataTableContext);
return (
<tbody>
{data.map((item, index) =>
children(item, index)
)}
</tbody>
);
}
// HOC for adding search functionality
function withSearch(WrappedComponent) {
return function SearchableComponent(props) {
const [searchTerm, setSearchTerm] = useState('');
const filteredProps = {
...props,
data: props.data.filter(item =>
Object.values(item).some(value =>
value.toString().toLowerCase().includes(searchTerm.toLowerCase())
)
)
};
return (
<div>
<input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
<WrappedComponent {...filteredProps} />
</div>
);
};
}
// Usage example
function UserManagement() {
const {
data,
addItem,
updateItem,
deleteItem,
setSorting,
sortBy,
sortDirection
} = useDataManager([
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User' },
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com', role: 'User' }
]);
const SearchableDataTable = withSearch(DataTable);
return (
<div className="user-management">
<h1>User Management</h1>
<SearchableDataTable
data={data}
onSort={setSorting}
sortBy={sortBy}
sortDirection={sortDirection}
>
<thead>
<tr>
<DataTableHeader sortKey="name">Name</DataTableHeader>
<DataTableHeader sortKey="email">Email</DataTableHeader>
<DataTableHeader sortKey="role">Role</DataTableHeader>
<th>Actions</th>
</tr>
</thead>
<DataTableBody>
{(user) => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.role}</td>
<td>
<button onClick={() => updateItem(user.id, { role: 'Admin' })}>
Make Admin
</button>
<button onClick={() => deleteItem(user.id)}>
Delete
</button>
</td>
</tr>
)}
</DataTableBody>
</SearchableDataTable>
</div>
);
}
// Challenge: Add pagination to the data table
// Challenge: Add bulk selection and operations
// Challenge: Add real-time updates with WebSocket
Test your understanding of this topic:
Continue your learning journey and master the next set of concepts.
Continue to Module 8