Master advanced React patterns including Higher-Order Components, Render Props, Compound Components, and advanced patterns for building reusable and maintainable components.
Master advanced React patterns including Higher-Order Components, Render Props, Compound Components, and advanced patterns for building reusable and maintainable components.
Master Higher-Order Components (HOCs) to create reusable logic and enhance components with cross-cutting concerns.
Content by: Praveen Kumar
MERN Stack Developer
Higher-Order Components are functions that take a component and return a new component with additional functionality. They are a powerful pattern for code reuse and logic sharing in React applications.
// Basic HOC example
function withLoading(WrappedComponent) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div>Loading...</div>;
}
return <WrappedComponent {...props} />;
};
}
// Usage
const UserProfile = ({ user }) => <div>{user.name}</div>;
const UserProfileWithLoading = withLoading(UserProfile);// HOC with state and lifecycle
function withData(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = { data: null, loading: true };
}
componentDidMount() {
this.fetchData();
}
fetchData = async () => {
try {
const data = await api.getData();
this.setState({ data, loading: false });
} catch (error) {
this.setState({ error, loading: false });
}
}
render() {
return <WrappedComponent {...this.props} {...this.state} />;
}
};
}// Authentication HOC
const withAuth = (WrappedComponent) => {
return function AuthenticatedComponent(props) {
const { user, isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <LoginPage />;
}
return <WrappedComponent {...props} user={user} />;
};
};
// Error Boundary HOC
const withErrorBoundary = (WrappedComponent) => {
return class extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by HOC:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <div>Something went wrong.</div>;
}
return <WrappedComponent {...this.props} />;
}
};
};// Complete data fetching HOC with caching
import React, { useState, useEffect, useCallback } from 'react';
const withDataFetching = (url, options = {}) => (WrappedComponent) => {
return function WithDataFetching(props) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [refetch, setRefetch] = useState(0);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, options);
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [url, JSON.stringify(options)]);
useEffect(() => {
fetchData();
}, [fetchData, refetch]);
const handleRefetch = () => setRefetch(prev => prev + 1);
return (
<WrappedComponent
{...props}
data={data}
loading={loading}
error={error}
refetch={handleRefetch}
/>
);
};
};
// Usage example
const UserList = ({ data, loading, error, refetch }) => {
if (loading) return <div>Loading users...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<button onClick={refetch}>Refresh</button>
<ul>
{data?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
};
const UserListWithData = withDataFetching('/api/users')(UserList);
export default UserListWithData;Test your understanding of this topic:
Learn the Render Props pattern to share code between components using a function as a child pattern.
Content by: Sachin Patel
Node.js 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 logic while maintaining flexibility.
// Basic render props example
const Mouse = ({ render }) => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (event) => {
setPosition({
x: event.clientX,
y: event.clientY
});
};
return (
<div onMouseMove={handleMouseMove}>
{render(position)}
</div>
);
};
// Usage
<Mouse render={({ x, y }) => (
<div>
Mouse position: {x}, {y}
</div>
)} />// Data fetching with render props
const DataFetcher = ({ url, children }) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, [url]);
return children({ data, loading, error });
};
// Usage
<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>// Toggle render prop
const Toggle = ({ children }) => {
const [isOn, setIsOn] = useState(false);
const toggle = () => setIsOn(!isOn);
return children({ isOn, toggle });
};
// Form render prop
const Form = ({ children, onSubmit }) => {
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
const handleChange = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
setErrors(prev => ({ ...prev, [name]: '' }));
};
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(values);
};
return children({
values,
errors,
handleChange,
handleSubmit
});
};// Complete modal render prop implementation
import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
const Modal = ({ isOpen, onClose, children }) => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
return () => setMounted(false);
}, []);
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
return () => {
document.body.style.overflow = 'unset';
};
}, [isOpen]);
if (!mounted || !isOpen) return null;
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>,
document.body
);
};
const ModalProvider = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
const [modalContent, setModalContent] = useState(null);
const openModal = (content) => {
setModalContent(content);
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
setModalContent(null);
};
return (
<>
{children({ openModal, closeModal })}
<Modal isOpen={isOpen} onClose={closeModal}>
{modalContent}
</Modal>
</>
);
};
// Usage
<ModalProvider>
{({ openModal, closeModal }) => (
<div>
<button onClick={() => openModal(<div>Modal Content</div>)}>
Open Modal
</button>
</div>
)}
</ModalProvider>Test your understanding of this topic:
Master compound components pattern to create flexible and composable component APIs that work together seamlessly.
Content by: Parth Patel
Node.js Developer
Compound components are a pattern where multiple components work together to form a complete UI. They share implicit state and provide a more flexible API than a single monolithic component.
// Basic compound component example
const Tabs = ({ children, defaultTab }) => {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<div className="tabs">
{React.Children.map(children, child =>
React.cloneElement(child, { activeTab, setActiveTab })
)}
</div>
);
};
const TabList = ({ children, activeTab, setActiveTab }) => (
<div className="tab-list">
{React.Children.map(children, (child, index) =>
React.cloneElement(child, {
isActive: activeTab === index,
onClick: () => setActiveTab(index)
})
)}
</div>
);
const Tab = ({ children, isActive, onClick }) => (
<button
className={`tab ${isActive ? 'active' : ''}`}
onClick={onClick}
>
{children}
</button>
);
const TabPanels = ({ children, activeTab }) => (
<div className="tab-panels">
{children[activeTab]}
</div>
);
const TabPanel = ({ children }) => (
<div className="tab-panel">
{children}
</div>
);
// Usage
<Tabs defaultTab={0}>
<TabList>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
</TabList>
<TabPanels>
<TabPanel>Content 1</TabPanel>
<TabPanel>Content 2</TabPanel>
</TabPanels>
</Tabs>// Using Context for compound components
const SelectContext = createContext();
const Select = ({ children, value, onChange }) => {
const [isOpen, setIsOpen] = useState(false);
const contextValue = {
value,
onChange,
isOpen,
setIsOpen
};
return (
<SelectContext.Provider value={contextValue}>
<div className="select">
{children}
</div>
</SelectContext.Provider>
);
};
const SelectTrigger = ({ children }) => {
const { isOpen, setIsOpen } = useContext(SelectContext);
return (
<button
className="select-trigger"
onClick={() => setIsOpen(!isOpen)}
>
{children}
</button>
);
};
const SelectContent = ({ children }) => {
const { isOpen } = useContext(SelectContext);
if (!isOpen) return null;
return (
<div className="select-content">
{children}
</div>
);
};
const SelectItem = ({ value, children }) => {
const { value: selectedValue, onChange, setIsOpen } = useContext(SelectContext);
const handleClick = () => {
onChange(value);
setIsOpen(false);
};
return (
<div
className={`select-item ${selectedValue === value ? 'selected' : ''}`}
onClick={handleClick}
>
{children}
</div>
);
};// Accordion compound component
const Accordion = ({ children, allowMultiple = false }) => {
const [openItems, setOpenItems] = useState(new Set());
const toggleItem = (itemId) => {
setOpenItems(prev => {
const newSet = new Set(prev);
if (newSet.has(itemId)) {
newSet.delete(itemId);
} else {
if (!allowMultiple) {
newSet.clear();
}
newSet.add(itemId);
}
return newSet;
});
};
return (
<AccordionContext.Provider value={{ openItems, toggleItem }}>
{children}
</AccordionContext.Provider>
);
};
const AccordionItem = ({ children, itemId }) => (
<div className="accordion-item">
{React.Children.map(children, child =>
React.cloneElement(child, { itemId })
)}
</div>
);
const AccordionTrigger = ({ children, itemId }) => {
const { openItems, toggleItem } = useContext(AccordionContext);
const isOpen = openItems.has(itemId);
return (
<button
className={`accordion-trigger ${isOpen ? 'open' : ''}`}
onClick={() => toggleItem(itemId)}
>
{children}
</button>
);
};// Complete form compound component system
import React, { createContext, useContext, useState } from 'react';
const FormContext = createContext();
const Form = ({ children, onSubmit, initialValues = {} }) => {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const setValue = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
const setError = (name, error) => {
setErrors(prev => ({ ...prev, [name]: error }));
};
const setTouchedField = (name) => {
setTouched(prev => ({ ...prev, [name]: true }));
};
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(values);
};
const contextValue = {
values,
errors,
touched,
setValue,
setError,
setTouchedField
};
return (
<FormContext.Provider value={contextValue}>
<form onSubmit={handleSubmit}>
{children}
</form>
</FormContext.Provider>
);
};
const FormField = ({ name, children }) => {
const { values, errors, touched, setValue, setTouchedField } = useContext(FormContext);
const value = values[name] || '';
const error = errors[name];
const isTouched = touched[name];
return (
<div className="form-field">
{React.Children.map(children, child =>
React.cloneElement(child, {
name,
value,
error,
isTouched,
onChange: (e) => setValue(name, e.target.value),
onBlur: () => setTouchedField(name)
})
)}
</div>
);
};
const FormInput = ({ name, value, error, isTouched, onChange, onBlur, ...props }) => (
<div>
<input
{...props}
name={name}
value={value}
onChange={onChange}
onBlur={onBlur}
className={`form-input ${error && isTouched ? 'error' : ''}`}
/>
{error && isTouched && (
<span className="form-error">{error}</span>
)}
</div>
);
const FormLabel = ({ children, ...props }) => (
<label className="form-label" {...props}>
{children}
</label>
);
const FormButton = ({ children, type = "submit", ...props }) => (
<button type={type} className="form-button" {...props}>
{children}
</button>
);
// Usage
<Form onSubmit={(values) => console.log(values)}>
<FormField name="email">
<FormLabel>Email</FormLabel>
<FormInput type="email" placeholder="Enter your email" />
</FormField>
<FormField name="password">
<FormLabel>Password</FormLabel>
<FormInput type="password" placeholder="Enter your password" />
</FormField>
<FormButton>Submit</FormButton>
</Form>Test your understanding of this topic:
Explore advanced React patterns including custom hooks composition, provider patterns, and performance optimization techniques.
Content by: Bansi Patel
Node.js Developer
Learn to compose multiple custom hooks together to create powerful, reusable logic that can be shared across components.
// Advanced provider pattern with multiple contexts
const AppContext = createContext();
const ThemeContext = createContext();
const AuthContext = createContext();
const AppProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
const login = async (credentials) => {
setIsLoading(true);
try {
const userData = await authService.login(credentials);
setUser(userData);
} catch (error) {
throw error;
} finally {
setIsLoading(false);
}
};
const logout = () => {
setUser(null);
authService.logout();
};
return (
<AppContext.Provider value={{ isLoading }}>
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
</ThemeContext.Provider>
</AppContext.Provider>
);
};// Complex custom hook with multiple concerns
const useAsyncOperation = (asyncFunction, dependencies = []) => {
const [state, setState] = useState({
data: null,
loading: false,
error: null
});
const execute = useCallback(async (...args) => {
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const result = await asyncFunction(...args);
setState({ data: result, loading: false, error: null });
return result;
} catch (error) {
setState({ data: null, loading: false, error });
throw error;
}
}, dependencies);
const reset = useCallback(() => {
setState({ data: null, loading: false, error: null });
}, []);
return { ...state, execute, reset };
};
// Usage
const UserProfile = ({ userId }) => {
const { data: user, loading, error, execute } = useAsyncOperation(
(id) => userService.getUser(id),
[userId]
);
useEffect(() => {
execute(userId);
}, [execute, userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user?.name}</div>;
};// Custom state management with reducer pattern
const useReducerWithMiddleware = (reducer, initialState, middleware = []) => {
const [state, dispatch] = useReducer(reducer, initialState);
const enhancedDispatch = useCallback((action) => {
let currentAction = action;
// Apply middleware
middleware.forEach(middlewareFn => {
currentAction = middlewareFn(currentAction, state);
});
dispatch(currentAction);
}, [state, middleware]);
return [state, enhancedDispatch];
};
// Logging middleware
const loggingMiddleware = (action, state) => {
console.log('Action:', action);
console.log('State before:', state);
return action;
};
// Usage
const [state, dispatch] = useReducerWithMiddleware(
counterReducer,
{ count: 0 },
[loggingMiddleware]
);// Complete data management system with caching and synchronization
import React, { createContext, useContext, useReducer, useCallback } from 'react';
const DataContext = createContext();
const dataReducer = (state, action) => {
switch (action.type) {
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_DATA':
return {
...state,
data: action.payload,
cache: { ...state.cache, [action.key]: action.payload }
};
case 'SET_ERROR':
return { ...state, error: action.payload };
case 'INVALIDATE_CACHE':
return { ...state, cache: {} };
default:
return state;
}
};
const DataProvider = ({ children }) => {
const [state, dispatch] = useReducer(dataReducer, {
data: null,
loading: false,
error: null,
cache: {}
});
const fetchData = useCallback(async (key, fetcher) => {
// Check cache first
if (state.cache[key]) {
dispatch({ type: 'SET_DATA', payload: state.cache[key], key });
return;
}
dispatch({ type: 'SET_LOADING', payload: true });
try {
const data = await fetcher();
dispatch({ type: 'SET_DATA', payload: data, key });
} catch (error) {
dispatch({ type: 'SET_ERROR', payload: error.message });
}
}, [state.cache]);
const invalidateCache = useCallback(() => {
dispatch({ type: 'INVALIDATE_CACHE' });
}, []);
return (
<DataContext.Provider value={{ ...state, fetchData, invalidateCache }}>
{children}
</DataContext.Provider>
);
};
const useData = (key, fetcher) => {
const { data, loading, error, fetchData } = useContext(DataContext);
const loadData = useCallback(() => {
fetchData(key, fetcher);
}, [key, fetcher, fetchData]);
return { data, loading, error, loadData };
};
export { DataProvider, useData };Test your understanding of this topic:
Continue your learning journey and master the next set of concepts.
Continue to Module 11