React Component Patterns and Design Patterns
1. Higher-Order Components (HOC) Pattern
| HOC Pattern | Syntax | Description | Use Case |
|---|---|---|---|
| Basic HOC | withFeature(Component) |
Function that takes component and returns enhanced component | Add props, wrap with context, inject dependencies |
| HOC with Arguments | withFeature(config)(Comp) |
Curried HOC that accepts configuration | Configurable behavior, conditional enhancement |
| Props Proxy | props => <Comp {...props}/> |
HOC manipulates props before passing to wrapped component | Transform, filter, or add props |
| Inheritance Inversion | class extends WrappedComp |
HOC extends wrapped component class DEPRECATED | Modify lifecycle, render - avoid, use hooks instead |
| Composing HOCs | compose(hoc1, hoc2)(Comp) |
Apply multiple HOCs in sequence | Combine multiple enhancements cleanly |
| Display Name | HOC.displayName = 'with...' |
Set display name for debugging | Better DevTools, error messages, stack traces |
Example: Basic HOC with authentication
// Basic HOC pattern
function withAuth(Component) {
return function AuthComponent(props) {
const { user, loading } = useAuth();
if (loading) return <LoadingSpinner />;
if (!user) return <Navigate to="/login" />;
return <Component {...props} user={user} />;
};
}
// Usage
const ProtectedDashboard = withAuth(Dashboard);
// HOC with configuration
function withPermission(permission) {
return function (Component) {
return function PermissionComponent(props) {
const { user } = useAuth();
if (!user?.permissions.includes(permission)) {
return <AccessDenied />;
}
return <Component {...props} />;
};
};
}
// Usage with config
const AdminPanel = withPermission('admin')(Panel);
Example: Props transformation and composition
// Props transformation HOC
function withUppercase(Component) {
return function UppercaseComponent({ name, ...props }) {
return <Component {...props} name={name?.toUpperCase()} />;
};
}
// Data fetching HOC
function withData(fetchFn, propName) {
return function (Component) {
return function DataComponent(props) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchFn().then((result) => {
setData(result);
setLoading(false);
});
}, []);
return <Component {...props} {...{[propName]: data}} loading={loading} />;
};
};
}
// Compose multiple HOCs
import { compose } from 'redux'; // or lodash/fp
const enhance = compose(
withAuth,
withData(fetchUserProfile, 'profile'),
withUppercase
);
const EnhancedProfile = enhance(ProfileComponent);
HOC Best Practices: Use descriptive names (with* prefix), pass all props through, set
displayName for debugging, prefer hooks for most cases, compose HOCs with utility functions, avoid mutating
wrapped component.
2. Render Props and Function-as-Children Pattern
| Pattern | Syntax | Description | Use Case |
|---|---|---|---|
| Render Prop | <Comp render={data => ...}/> |
Pass function as prop that returns React element | Share logic while giving render control to consumer |
| Children as Function | <Comp>{data => ...}</Comp> |
Children prop is a function receiving data | More ergonomic API, follows React conventions |
| Multiple Render Props | header={...} footer={...} |
Multiple function props for different render areas | Flexible layout with pluggable sections |
| Render Props with Hooks | useData().render() |
Hooks replaced most render prop use cases PREFERRED | Modern alternative - cleaner, more composable |
Example: Mouse tracker with render prop
// Render prop component
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return render(position);
}
// Usage with render prop
<MouseTracker
render={({ x, y }) => (
<h1>Mouse at {x}, {y}</h1>
)}
/>
// Children as function pattern
function DataProvider({ url, children }) {
const [data, loading, error] = useFetch(url);
return children({ data, loading, error });
}
// Usage
<DataProvider url="/api/users">
{({ data, loading, error }) => {
if (loading) return <Spinner />;
if (error) return <Error message={error} />;
return <UserList users={data} />;
}}
</DataProvider>
Example: Modern hook alternative (preferred)
// Custom hook replaces render prop pattern
function useMouse() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return position;
}
// Usage - much cleaner!
function MyComponent() {
const { x, y } = useMouse();
return <h1>Mouse at {x}, {y}</h1>;
}
Warning: Render props can hurt performance if function is recreated on each render. Use
useCallback for render prop functions. Consider custom hooks as modern alternative.
3. Compound Components Pattern and Component APIs
| Pattern | Syntax | Description | Use Case |
|---|---|---|---|
| Compound Components | Menu.Item, Menu.Divider |
Components designed to work together, share implicit state | Complex UI with coordinated subcomponents |
| Static Properties | Component.SubComponent |
Attach child components as static properties of parent | Namespace related components, cleaner imports |
| Context-based | Context.Provider + useContext |
Share state via context between parent and children | Flexible structure, works at any nesting level |
| Clone Element | React.cloneElement(child) |
Clone and inject props into children | Add props to unknown children, less flexible |
| Controlled Compound | activeItem={...} onChange |
Parent controls shared state externally | Integrate with forms, external state management |
Example: Tabs compound component with context
// Context for shared state
const TabContext = createContext();
function Tabs({ children, defaultTab }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabContext.Provider>
);
}
function TabList({ children }) {
return <div className="tab-list" role="tablist">{children}</div>;
}
function Tab({ id, children }) {
const { activeTab, setActiveTab } = useContext(TabContext);
const isActive = activeTab === id;
return (
<button
role="tab"
aria-selected={isActive}
onClick={() => setActiveTab(id)}
className={isActive ? 'active' : ''}
>
{children}
</button>
);
}
function TabPanel({ id, children }) {
const { activeTab } = useContext(TabContext);
if (activeTab !== id) return null;
return <div role="tabpanel">{children}</div>;
}
// Attach as static properties
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
// Usage - declarative API
<Tabs defaultTab="home">
<Tabs.List>
<Tabs.Tab id="home">Home</Tabs.Tab>
<Tabs.Tab id="profile">Profile</Tabs.Tab>
<Tabs.Tab id="settings">Settings</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id="home">Home content</Tabs.Panel>
<Tabs.Panel id="profile">Profile content</Tabs.Panel>
<Tabs.Panel id="settings">Settings content</Tabs.Panel>
</Tabs>
Example: Controlled compound component
// Controlled version for external state control
function ControlledTabs({ children, activeTab, onTabChange }) {
return (
<TabContext.Provider value={{ activeTab, setActiveTab: onTabChange }}>
<div className="tabs">{children}</div>
</TabContext.Provider>
);
}
// Usage with external state
function App() {
const [tab, setTab] = useState('home');
return (
<>
<button onClick={() => setTab('profile')}>
Go to Profile (external)
</button>
<ControlledTabs activeTab={tab} onTabChange={setTab}>
<Tabs.List>
<Tabs.Tab id="home">Home</Tabs.Tab>
<Tabs.Tab id="profile">Profile</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id="home">Home</Tabs.Panel>
<Tabs.Panel id="profile">Profile</Tabs.Panel>
</ControlledTabs>
</>
);
}
Compound Component Benefits: Flexible, declarative API; implicit state sharing; easy to extend;
semantic JSX structure; reduced prop drilling; better separation of concerns.
4. Custom Hooks Pattern for Logic Reuse
| Hook Pattern | Purpose | Returns | Use Case |
|---|---|---|---|
| State Hook | Encapsulate stateful logic | [state, setState] |
Toggle, counter, form state, modal open/close |
| Effect Hook | Side effects and subscriptions | void or cleanup |
Event listeners, timers, subscriptions, polling |
| Data Hook | Data fetching and caching | { data, loading, error } |
API calls, async data, pagination, infinite scroll |
| Computation Hook | Derived state and calculations | computedValue |
Filtering, sorting, validation, formatting |
| Ref Hook | DOM access, stable refs | ref |
Focus management, measurements, previous values |
| Composite Hook | Combine multiple hooks | { ...multiple values } |
Complex workflows, coordinated state |
Example: Custom hooks for common patterns
// Toggle hook
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return [value, toggle, setTrue, setFalse];
}
// Usage
const [isOpen, toggle, open, close] = useToggle();
// Local storage hook
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
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 saving to localStorage:', error);
}
}, [key, storedValue]);
return [storedValue, setValue];
}
// Previous value hook
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
Example: Data fetching and debounce hooks
// Fetch hook with loading/error states
function useFetch(url, options) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isCancelled = false;
setLoading(true);
fetch(url, options)
.then(res => res.json())
.then(data => {
if (!isCancelled) {
setData(data);
setError(null);
}
})
.catch(err => {
if (!isCancelled) {
setError(err.message);
setData(null);
}
})
.finally(() => {
if (!isCancelled) setLoading(false);
});
return () => { isCancelled = true; };
}, [url, JSON.stringify(options)]);
return { data, loading, error };
}
// Debounce hook
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// Usage - search with debounce
function SearchComponent() {
const [search, setSearch] = useState('');
const debouncedSearch = useDebounce(search, 500);
const { data, loading } = useFetch(`/api/search?q=${debouncedSearch}`);
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
{loading ? <Spinner /> : <Results data={data} />}
</div>
);
}
Custom Hook Guidelines: Always start with 'use' prefix, follow hooks rules, return
values/functions consumers need, use useCallback for returned functions, document parameters and return values,
handle cleanup in useEffect.
5. Polymorphic Components and Generic TypeScript Props
| Pattern | Implementation | Description | Use Case |
|---|---|---|---|
| As Prop Pattern | <Box as="button"> |
Component renders as different HTML elements | Flexible primitive components, design systems |
| Component Prop | <Comp component={Link}> |
Accept component as prop to customize rendering | Wrapper components, list items, custom elements |
| Render Function | renderItem={(item) => ...} |
Function prop that returns rendered content | Lists, grids, custom rendering logic |
| TypeScript Generic | <T extends ElementType> |
Type-safe polymorphic components with TS TS | Fully typed props based on element type |
Example: Polymorphic Box component
// Basic polymorphic component
function Box({ as: Component = 'div', children, ...props }) {
return <Component {...props}>{children}</Component>;
}
// Usage - renders as different elements
<Box>Default div</Box>
<Box as="section">Section element</Box>
<Box as="button" onClick={handleClick}>Button element</Box>
<Box as={Link} to="/home">React Router Link</Box>
// TypeScript polymorphic component with full type safety
import { ElementType, ComponentPropsWithoutRef } from 'react';
type PolymorphicProps<T extends ElementType> = {
as?: T;
children?: React.ReactNode;
} & ComponentPropsWithoutRef<T>;
function PolymorphicBox<T extends ElementType = 'div'>({
as,
children,
...props
}: PolymorphicProps<T>) {
const Component = as || 'div';
return <Component {...props}>{children}</Component>;
}
// TypeScript enforces correct props based on 'as' prop
<PolymorphicBox as="a" href="/path">Link</PolymorphicBox> // ✓ href allowed
<PolymorphicBox as="button" onClick={fn}>Button</PolymorphicBox> // ✓ onClick allowed
<PolymorphicBox as="div" href="/path">Div</PolymorphicBox> // ✗ TS error - href not valid
Example: Generic list component
// Generic list with custom rendering
function List({ items, renderItem, emptyMessage = 'No items' }) {
if (!items?.length) {
return <div className="empty">{emptyMessage}</div>;
}
return (
<ul className="list">
{items.map((item, index) => (
<li key={item.id || index}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// Usage with different render functions
<List
items={users}
renderItem={(user) => <UserCard user={user} />}
/>
<List
items={products}
renderItem={(product, idx) => (
<div>
{idx + 1}. {product.name} - ${product.price}
</div>
)}
/>
// With component prop pattern
function Container({ component: Component = 'div', children, ...props }) {
return (
<Component className="container" {...props}>
{children}
</Component>
);
}
// Usage
<Container>Default div container</Container>
<Container component="section">Section container</Container>
<Container component={Card}>Custom component</Container>
Polymorphic Component Benefits: Single component, multiple elements; reduces component
proliferation; type-safe with TypeScript; flexible for design systems; maintains consistent styling/behavior.
6. Component Composition vs Inheritance - React Philosophy
| Approach | React Recommendation | Pattern | Reason |
|---|---|---|---|
| Composition PREFERRED | Strongly recommended | Children prop, props, slots | More flexible, easier to understand, React way |
| Inheritance AVOID | Not recommended | Class extends Component | Tight coupling, less flexible, not idiomatic React |
| Containment | Use children prop | <Box>{children}</Box> |
Generic containers, wrappers, layouts |
| Specialization | Compose with props | <Dialog type="alert"> |
Specific variants from generic components |
| Named Slots | Multiple props | header={...} footer={...} |
Complex layouts with multiple sections |
Example: Composition patterns (preferred)
// Containment - generic container with children
function Dialog({ title, children, footer }) {
return (
<div className="dialog">
<div className="dialog-header">
<h2>{title}</h2>
</div>
<div className="dialog-body">
{children}
</div>
{footer && (
<div className="dialog-footer">
{footer}
</div>
)}
</div>
);
}
// Specialization - specific dialog variants via composition
function WelcomeDialog() {
return (
<Dialog
title="Welcome"
footer={
<button onClick={handleStart}>Get Started</button>
}
>
<p>Thank you for joining!</p>
</Dialog>
);
}
function ConfirmDialog({ onConfirm, onCancel }) {
return (
<Dialog
title="Confirm Action"
footer={
<>
<button onClick={onCancel}>Cancel</button>
<button onClick={onConfirm}>Confirm</button>
</>
}
>
<p>Are you sure you want to proceed?</p>
</Dialog>
);
}
// Named slots pattern
function Layout({ header, sidebar, main, footer }) {
return (
<div className="layout">
<header>{header}</header>
<div className="layout-body">
<aside>{sidebar}</aside>
<main>{main}</main>
</div>
<footer>{footer}</footer>
</div>
);
}
// Usage
<Layout
header={<Header />}
sidebar={<Sidebar />}
main={<MainContent />}
footer={<Footer />}
/>
Example: Why composition beats inheritance
// ❌ Inheritance approach (avoid)
class BaseDialog extends Component {
render() {
return (
<div className="dialog">
<h2>{this.props.title}</h2>
{this.renderContent()}
{this.renderActions()}
</div>
);
}
}
class WelcomeDialog extends BaseDialog {
renderContent() {
return <p>Welcome!</p>;
}
renderActions() {
return <button>Get Started</button>;
}
}
// Issues: Tight coupling, hard to modify, fragile base class
// ✓ Composition approach (preferred)
function Dialog({ title, children, actions }) {
return (
<div className="dialog">
<h2>{title}</h2>
<div>{children}</div>
<div>{actions}</div>
</div>
);
}
function WelcomeDialog() {
return (
<Dialog
title="Welcome"
actions={<button>Get Started</button>}
>
<p>Welcome!</p>
</Dialog>
);
}
// Benefits: Flexible, clear, easy to modify, testable
// Composition with hooks for behavior reuse
function useDialogState() {
const [isOpen, setIsOpen] = useState(false);
return { isOpen, open: () => setIsOpen(true), close: () => setIsOpen(false) };
}
function MyComponent() {
const dialog = useDialogState();
return (
<>
<button onClick={dialog.open}>Open Dialog</button>
{dialog.isOpen && (
<Dialog title="My Dialog" onClose={dialog.close}>
Content here
</Dialog>
)}
</>
);
}
Warning: React does NOT recommend using inheritance for component reuse. Props and composition
give you all the flexibility you need. Use hooks for behavior reuse, not class inheritance.