
How to Create Wrapper Components in React with Props
React wrapper components are a fundamental pattern that allows you to encapsulate common functionality, styling, or layout logic while maintaining the flexibility to pass different content as children or props. They act as containers that wrap other components or elements, providing a clean way to share functionality across your application without repeating code. This post will walk you through creating effective wrapper components, handling props properly, avoiding common pitfalls, and implementing real-world examples that you can use in production applications.
Understanding Wrapper Components and How They Work
Wrapper components follow the composition pattern in React, where you build complex UIs by combining simpler components. The key concept is using the children
prop, which React automatically passes to components and contains whatever JSX is placed between the component’s opening and closing tags.
Here’s the basic anatomy of a wrapper component:
function BasicWrapper({ children, className }) {
return (
<div className={`wrapper ${className || ''}`}>
{children}
</div>
);
}
// Usage
<BasicWrapper className="custom-style">
<h1>This content gets passed as children</h1>
<p>Any JSX can go here</p>
</BasicWrapper>
The magic happens through React’s reconciliation process. When you place JSX between component tags, React creates a virtual DOM representation and passes it as the children
prop. The wrapper component then decides where and how to render these children within its own JSX structure.
Step-by-Step Implementation Guide
Let’s build a comprehensive wrapper component that handles multiple scenarios you’ll encounter in real applications.
Basic Card Wrapper
import React from 'react';
import './Card.css';
function Card({ children, title, variant = 'default', shadow = true, ...restProps }) {
const cardClasses = [
'card',
`card--${variant}`,
shadow ? 'card--shadow' : ''
].filter(Boolean).join(' ');
return (
<div className={cardClasses} {...restProps}>
{title && <div className="card__header">{title}</div>}
<div className="card__content">
{children}
</div>
</div>
);
}
export default Card;
Advanced Modal Wrapper with Portal
import React, { useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
function Modal({
children,
isOpen,
onClose,
title,
size = 'medium',
closeOnEscape = true,
closeOnOverlayClick = true
}) {
const modalRef = useRef(null);
useEffect(() => {
if (!closeOnEscape) return;
const handleEscape = (e) => {
if (e.key === 'Escape') {
onClose();
}
};
if (isOpen) {
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = 'unset';
};
}, [isOpen, closeOnEscape, onClose]);
const handleOverlayClick = (e) => {
if (closeOnOverlayClick && e.target === e.currentTarget) {
onClose();
}
};
if (!isOpen) return null;
const modalContent = (
<div className="modal-overlay" onClick={handleOverlayClick}>
<div className={`modal modal--${size}`} ref={modalRef}>
<div className="modal__header">
{title && <h2 className="modal__title">{title}</h2>}
<button
className="modal__close"
onClick={onClose}
aria-label="Close modal"
>
Γ
</button>
</div>
<div className="modal__content">
{children}
</div>
</div>
</div>
);
return ReactDOM.createPortal(modalContent, document.body);
}
export default Modal;
Layout Wrapper with Error Boundaries
import React from 'react';
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('ErrorBoundary caught an error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
</div>
);
}
return this.props.children;
}
}
function PageLayout({ children, sidebar, header, footer, loading = false }) {
if (loading) {
return (
<div className="page-layout page-layout--loading">
<div className="loading-spinner">Loading...</div>
</div>
);
}
return (
<ErrorBoundary>
<div className="page-layout">
{header && (
<header className="page-layout__header">
{header}
</header>
)}
<div className="page-layout__body">
{sidebar && (
<aside className="page-layout__sidebar">
{sidebar}
</aside>
)}
<main className="page-layout__main">
{children}
</main>
</div>
{footer && (
<footer className="page-layout__footer">
{footer}
</footer>
)}
</div>
</ErrorBoundary>
);
}
export default PageLayout;
Real-World Examples and Use Cases
Here are practical implementations you can adapt for your projects:
API Data Wrapper
import React, { useState, useEffect } from 'react';
function DataWrapper({
url,
children,
loadingComponent = <div>Loading...</div>,
errorComponent = null,
dependencies = []
}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
async function fetchData() {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (isMounted) {
setData(result);
setLoading(false);
}
} catch (err) {
if (isMounted) {
setError(err.message);
setLoading(false);
}
}
}
fetchData();
return () => {
isMounted = false;
};
}, [url, ...dependencies]);
if (loading) return loadingComponent;
if (error) {
return errorComponent || (
<div className="error-wrapper">
<p>Error: {error}</p>
<button onClick={() => window.location.reload()}>
Retry
</button>
</div>
);
}
return children({ data, refetch: () => window.location.reload() });
}
// Usage
<DataWrapper url="/api/users">
{({ data, refetch }) => (
<div>
<button onClick={refetch}>Refresh</button>
{data.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
)}
</DataWrapper>
Form Wrapper with Validation
import React, { useState, useContext, createContext } from 'react';
const FormContext = createContext();
function FormWrapper({
children,
onSubmit,
validationRules = {},
initialValues = {}
}) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const validateField = (name, value) => {
const rules = validationRules[name];
if (!rules) return '';
for (const rule of rules) {
const error = rule(value, values);
if (error) return error;
}
return '';
};
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
// Validate all fields
const newErrors = {};
Object.keys(validationRules).forEach(field => {
const error = validateField(field, values[field]);
if (error) newErrors[field] = error;
});
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) {
try {
await onSubmit(values);
} catch (error) {
console.error('Form submission error:', error);
}
}
setIsSubmitting(false);
};
const contextValue = {
values,
errors,
isSubmitting,
setFieldValue: (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
},
setFieldError: (name, error) => {
setErrors(prev => ({ ...prev, [name]: error }));
}
};
return (
<FormContext.Provider value={contextValue}>
<form onSubmit={handleSubmit} noValidate>
{children}
</form>
</FormContext.Provider>
);
}
function FormField({ name, label, type = 'text', required = false }) {
const { values, errors, setFieldValue } = useContext(FormContext);
return (
<div className="form-field">
<label htmlFor={name}>
{label}
{required && <span className="required">*</span>}
</label>
<input
id={name}
type={type}
value={values[name] || ''}
onChange={(e) => setFieldValue(name, e.target.value)}
className={errors[name] ? 'error' : ''}
/>
{errors[name] && (
<span className="error-message">{errors[name]}</span>
)}
</div>
);
}
export { FormWrapper, FormField };
Comparison with Alternative Patterns
Pattern | Use Case | Props Complexity | Reusability | Performance |
---|---|---|---|---|
Wrapper Components | Layout, common functionality | Low to Medium | High | Good |
Render Props | Complex state sharing | Medium | High | Good |
Higher-Order Components | Cross-cutting concerns | High | Medium | Fair (extra nesting) |
Custom Hooks | Stateful logic | Low | High | Excellent |
Context API | Global state | Low | Medium | Fair (re-renders) |
Best Practices and Common Pitfalls
Props Handling Best Practices
- Always use prop destructuring with default values to prevent undefined errors
- Implement proper TypeScript interfaces for better development experience
- Use the spread operator (…restProps) to pass through unhandled props
- Validate props using PropTypes in development or TypeScript in production
- Avoid prop drilling by combining wrappers with Context API when necessary
// Good prop handling example
function OptimizedWrapper({
children,
className = '',
variant = 'default',
testId,
...restProps
}) {
return (
<div
className={`wrapper wrapper--${variant} ${className}`.trim()}
data-testid={testId}
{...restProps}
>
{children}
</div>
);
}
Performance Considerations
Wrapper components can impact performance if not implemented carefully. Here are key optimization strategies:
import React, { memo, useMemo } from 'react';
const OptimizedWrapper = memo(({
children,
expensiveCalculation,
style = {},
...props
}) => {
// Memoize expensive calculations
const computedStyle = useMemo(() => {
return {
...style,
transform: expensiveCalculation ? `scale(${expensiveCalculation})` : 'none'
};
}, [style, expensiveCalculation]);
return (
<div style={computedStyle} {...props}>
{children}
</div>
);
});
OptimizedWrapper.displayName = 'OptimizedWrapper';
Common Pitfalls to Avoid
- Overusing wrappers: Don’t create wrapper components for simple one-off styling. Use CSS classes instead
- Prop name conflicts: Be careful when spreading props to avoid overriding internal component props
- Missing key props: When rendering lists inside wrappers, ensure proper key handling
- Event handling issues: Don’t accidentally prevent event bubbling in wrapper components
- Accessibility oversights: Maintain proper ARIA attributes and semantic HTML structure
// Problematic wrapper
function BadWrapper({ onClick, children, ...props }) {
const handleClick = (e) => {
e.stopPropagation(); // This breaks event bubbling!
onClick?.(e);
};
return <div onClick={handleClick} {...props}>{children}</div>;
}
// Better approach
function GoodWrapper({ onClick, children, ...props }) {
const handleClick = (e) => {
onClick?.(e);
// Let event continue bubbling naturally
};
return <div onClick={handleClick} {...props}>{children}</div>;
}
Testing Wrapper Components
Testing wrapper components requires special attention to both the wrapper logic and the children rendering:
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import Modal from './Modal';
describe('Modal Wrapper', () => {
test('renders children when open', () => {
render(
<Modal isOpen={true} onClose={jest.fn()}>
<p>Modal content</p>
</Modal>
);
expect(screen.getByText('Modal content')).toBeInTheDocument();
});
test('handles escape key closing', () => {
const onClose = jest.fn();
render(
<Modal isOpen={true} onClose={onClose} closeOnEscape={true}>
<p>Modal content</p>
</Modal>
);
fireEvent.keyDown(document, { key: 'Escape' });
expect(onClose).toHaveBeenCalledTimes(1);
});
test('passes through custom props', () => {
render(
<Modal
isOpen={true}
onClose={jest.fn()}
data-testid="custom-modal"
>
<p>Content</p>
</Modal>
);
expect(screen.getByTestId('custom-modal')).toBeInTheDocument();
});
});
For applications requiring robust hosting solutions, consider deploying your React applications on reliable infrastructure like VPS services or dedicated servers for optimal performance and scalability.
Wrapper components are essential building blocks in modern React applications. They promote code reuse, maintain consistency, and provide clean abstractions for complex functionality. When implemented with proper prop handling, performance optimization, and testing, they become powerful tools for building maintainable and scalable user interfaces. The key is finding the right balance between flexibility and simplicity, ensuring your wrappers solve real problems without adding unnecessary complexity to your codebase.
For more detailed information about React patterns and component composition, check out the official React documentation on passing props to components and composition vs inheritance.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.