Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// HOC that adds authentication gating
function withAuth<P extends object>(
WrappedComponent: React.ComponentType<P>
) {
function AuthenticatedComponent(props: P) {
const { user, isLoading } = useAuth();
if (isLoading) return <LoadingSpinner />;
if (!user) return <Navigate to="/login" />;
return <WrappedComponent {...props} />;
}
AuthenticatedComponent.displayName =
`withAuth(${WrappedComponent.displayName ?? WrappedComponent.name ?? "Component"})`;
return AuthenticatedComponent;
}
// Usage
const ProtectedDashboard = withAuth(Dashboard);When to reach for this: When you need to apply the same cross-cutting concern (auth, logging, theming, data fetching) to many components without modifying them. Less common today thanks to hooks, but still useful for route-level wrappers and third-party library integration.
import { useEffect, useRef, type ComponentType } from "react";
// HOC that tracks component visibility and reports analytics
function withVisibilityTracking<P extends { id: string }>(
WrappedComponent: ComponentType<P>,
eventName: string
) {
function TrackedComponent(props: P) {
const ref = useRef<HTMLDivElement>(null);
const reported = useRef(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !reported.current) {
reported.current = true;
analytics.track(eventName, { componentId: props.id });
}
},
{ threshold: 0.5 }
);
observer.observe(element);
return () => observer.disconnect();
}, [props.id]);
return (
<div ref={ref}>
<WrappedComponent {...props} />
</div>
);
}
TrackedComponent.displayName =
`withVisibilityTracking(${WrappedComponent.displayName ?? WrappedComponent.name ?? "Component"})`;
return TrackedComponent;
}
// Usage
interface ProductCardProps {
id: string;
name: string;
price: number;
}
function ProductCard({ id, name, price }: ProductCardProps) {
return (
<div className="p-4 border rounded">
<h3>{name}</h3>
<p>${price}</p>
</div>
);
}
const TrackedProductCard = withVisibilityTracking(ProductCard, "product_viewed");
// In a page
function ProductGrid({ products }: { products: ProductCardProps[] }) {
return (
<div className="grid grid-cols-3 gap-4">
{products.map((p) => (
<TrackedProductCard key={p.id} {...p} />
))}
</div>
);
}What this demonstrates:
P extends { id: string }) ensures the wrapped component has required propsdisplayName set for React DevTools debuggingProductCard remains pure and testable on its ownenhance(Component) => EnhancedComponent.withAuth(withTheme(withAnalytics(Component))).| Parameter | Type | Purpose |
|---|---|---|
WrappedComponent | ComponentType<P> | The component to enhance |
| Config (optional) | Varies | Configuration for the HOC behavior |
| Return value | ComponentType<P> (or modified) | A new component with added behavior |
Props injection HOC - adds new props to the wrapped component:
interface WithThemeProps {
theme: Theme;
}
function withTheme<P extends WithThemeProps>(
WrappedComponent: ComponentType<P>
) {
function ThemedComponent(props: Omit<P, keyof WithThemeProps>) {
const theme = useTheme();
return <WrappedComponent {...(props as P)} theme={theme} />;
}
ThemedComponent.displayName =
`withTheme(${WrappedComponent.displayName ?? WrappedComponent.name})`;
return ThemedComponent;
}Composed HOCs - combine multiple enhancements:
// Manual composition
const EnhancedComponent = withAuth(withTheme(withAnalytics(BaseComponent)));
// With a compose utility
import { compose } from "redux"; // or write your own
const enhance = compose(withAuth, withTheme, withAnalytics);
const EnhancedComponent = enhance(BaseComponent);ComponentType<P> to accept both function and class components.Omit<P, keyof InjectedProps> to remove injected props from the external API.P extends object at minimum to avoid primitive types.displayName for every HOC to make React DevTools usable.React.forwardRef inside HOCs if the wrapped component needs ref forwarding.Ref forwarding - Refs do not pass through HOCs automatically because ref is not a regular prop. Fix: Use React.forwardRef inside the HOC wrapper.
Static methods lost - Static properties on the original component are not copied to the wrapper. Fix: Use hoist-non-react-statics or manually copy needed statics.
Prop name collisions - If the HOC injects a prop with the same name as an existing prop, it silently overwrites. Fix: Namespace injected props or use a unique prefix.
Re-creating the HOC on every render - Calling a HOC inside a component body creates a new component type each render, destroying all state. Fix: Always call HOCs at module scope or in a useMemo with extreme caution.
Debugging difficulty - Deeply nested HOCs create long component trees in DevTools. Fix: Set displayName on every HOC and consider whether hooks would be clearer.
| Approach | Trade-off |
|---|---|
| Higher-order components | Transparent enhancement; typing is complex, debugging harder |
| Custom hooks | Simpler, composable, better types; cannot wrap rendering |
| Render props | Explicit data flow; more verbose at call site |
| Middleware (Next.js) | Better for route-level concerns like auth in Next.js apps |
| Decorators (stage 3) | Syntactic sugar; not yet widely supported in React ecosystem |
enhance(Component) => EnhancedComponent.// Manual composition
const Enhanced = withAuth(withTheme(withAnalytics(Base)));
// With a compose utility
const enhance = compose(withAuth, withTheme, withAnalytics);
const Enhanced = enhance(Base);compose utility.withAnalytics is applied first, then withTheme, then withAuth.displayName, React DevTools shows the wrapper component as "Anonymous" or the wrapper's internal name.displayName to withAuth(Dashboard) makes the component tree readable.displayName or name property.ref is not a regular prop in React; it is handled specially and stripped before reaching the wrapped component.React.forwardRef inside the HOC to forward refs to the wrapped component.interface WithThemeProps { theme: Theme; }
function withTheme<P extends WithThemeProps>(
WrappedComponent: ComponentType<P>
) {
function Themed(props: Omit<P, keyof WithThemeProps>) {
const theme = useTheme();
return <WrappedComponent {...(props as P)} theme={theme} />;
}
return Themed;
}Omit<P, keyof InjectedProps> to remove injected props from the external API.P extends InjectedProps so TypeScript knows the wrapped component accepts those props.Omit<P, keyof InjectedProps> removes the injected prop names from the type the consumer sees.theme when using withTheme(Component) because the HOC provides it.P extends object at minimum to prevent primitive type parameters.hoist-non-react-statics package or manually copy needed statics.getStaticProps.themeConfig instead of config) or use a unique prefix.Reviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥