Advanced React Patterns: Design System Foundations
These conceptsโRender Props, Wrapper Components, and Polymorphic Componentsโare the building blocks of Design Systems and Headless UI libraries. If you work on a team building a reusable component library, these are mandatory skills.
๐ญ Module 1: Render Props (The "Headless" Pattern)
Ready to unlock ultimate component flexibility? ๐ Render Props let YOU decide what gets rendered while the component handles all the messy logic. It's like hiring a chef who preps all the ingredients but lets you plate the dish however you want. This is the secret sauce behind Radix UI, React Aria, and every serious headless library!
๐ Impact: HIGH โ Render Props are the backbone of headless UI libraries โ master this and you can build anything! ๐๏ธ
๐ In this section: Inversion of Control โข SortableList Pattern โข Toggle Component โข Children as Function
// A Sortable List that handles complex logic but lets YOU draw the rowstype SortableListProps<T> = {items: T[];// ๐ The Render Prop: We give you the item, you give us JSX.renderItem: (item: T) => React.ReactNode;};// This component handles logic (mapping) but enforces ZERO style.export const SortableList = <T extends { id: string | number }>({items,renderItem}: SortableListProps<T>) => {return (<ul className="divide-y divide-gray-200">{items.map((item) => (<li key={item.id} className="p-4 hover:bg-gray-50">{/* We invoke the render prop here */}{renderItem(item)}</li>))}</ul>);};// Usage Example 1: Users Listtype User = { id: string; name: string; role: string };const UsersList = ({ users }: { users: User[] }) => {return (<SortableListitems={users}renderItem={(user) => (<div className="flex justify-between"><b>{user.name}</b><span className="text-gray-500">{user.role}</span></div>)}/>);};// Usage Example 2: Products List (Same component, different rendering!)type Product = { id: number; name: string; price: number };const ProductsList = ({ products }: { products: Product[] }) => {return (<SortableListitems={products}renderItem={(product) => (<div className="flex items-center gap-4"><img src={product.image} alt={product.name} className="w-12 h-12" /><div className="flex-1"><h3 className="font-semibold">{product.name}</h3><p className="text-sm text-gray-600">${product.price}</p></div></div>)}/>);};// Advanced: Render Props with Statetype ToggleProps = {children: (props: { isOn: boolean; toggle: () => void }) => React.ReactNode;};export const Toggle = ({ children }: ToggleProps) => {const [isOn, setIsOn] = React.useState(false);const toggle = () => setIsOn(prev => !prev);return <>{children({ isOn, toggle })}</>;};// Usage: Complete control over rendering<Toggle>{({ isOn, toggle }) => (<button onClick={toggle} className={isOn ? 'bg-green-500' : 'bg-gray-300'}>{isOn ? 'ON' : 'OFF'}</button>)}</Toggle>
๐ก๏ธ Module 2: Wrapper Components (The "Guard" Pattern)
Stop copy-pasting if (isAdmin) checks everywhere! ๐ Wrapper Components are your app's bouncers โ they decide who gets in and what gets shown. Stack them like building blocks: AuthGuard > FeatureGuard > Layout > Page. Clean, composable, and SO satisfying to use.
๐ Impact: HIGH โ Wrapper components prevent security holes and eliminate hundreds of lines of repetitive conditional logic! ๐
๐ In this section: FeatureGuard Pattern โข AuthGuard Pattern โข Composable Wrappers โข Error Boundary Wrapper
// A Feature Guard Wrapper that handles permissions, loading, and redirectstype FeatureGuardProps = {featureFlag: string;fallback?: React.ReactNode; // What to show if access deniedchildren: React.ReactNode;};export const FeatureGuard = ({featureFlag,fallback = <AccessDenied />,children}: FeatureGuardProps) => {const { user, isLoading } = useUser();const { flags } = useFeatureFlags();if (isLoading) return <FullPageSpinner />;// 1. Check if the feature is enabled globallyif (!flags[featureFlag]) return null;// 2. Check if user has permissionif (!user?.permissions.includes(featureFlag)) {return <>{fallback}</>;}// 3. If all checks pass, render the protected contentreturn <>{children}</>;};// Usage: This entire section is protected. No 'if' statements needed inside Dashboard.<FeatureGuard featureFlag="beta_dashboard"><NewBetaDashboard /></FeatureGuard>// Advanced: Composable Wrapperstype AuthGuardProps = {requiredRole?: string;redirectTo?: string;children: React.ReactNode;};export const AuthGuard = ({requiredRole,redirectTo = '/login',children}: AuthGuardProps) => {const { user, isLoading } = useAuth();const router = useRouter();useEffect(() => {if (!isLoading && !user) {router.push(redirectTo);}}, [user, isLoading, router, redirectTo]);if (isLoading) return <LoadingSpinner />;if (!user) return null;if (requiredRole && user.role !== requiredRole) {return <AccessDenied message="You don't have the required role" />;}return <>{children}</>;};// Usage: Stack multiple wrappers for complex requirements<AuthGuard requiredRole="admin"><FeatureGuard featureFlag="advanced_analytics"><Layout><AdvancedAnalyticsDashboard /></Layout></FeatureGuard></AuthGuard>// Another Example: Error Boundary Wrappertype ErrorBoundaryWrapperProps = {fallback?: React.ReactNode;children: React.ReactNode;};export const ErrorBoundaryWrapper = ({fallback = <ErrorFallback />,children}: ErrorBoundaryWrapperProps) => {return (<ErrorBoundary fallback={fallback}>{children}</ErrorBoundary>);};
๐ฆ Module 3: Polymorphic Components (The "Design System" Pattern)
Shapeshifting components! ๐ฆธ Imagine a Button that can be a <button>, an <a>, or even a <div> โ all with the same styles and full TypeScript support. The 'as' prop pattern is how Material UI, Chakra, and Mantine build their magic. Once you learn this, you'll see it EVERYWHERE.
๐ด Impact: CRITICAL โ This is THE pattern behind every major design system โ learn it to build truly reusable component libraries! ๐
๐ In this section: The 'as' Prop โข ComponentPropsWithoutRef โข Polymorphic Text โข Polymorphic Button โข Polymorphic Card
import { ComponentPropsWithoutRef, ElementType } from "react";// 1. Define props, allowing an 'as' prop (defaulting to 'span')type TextProps<T extends ElementType> = {as?: T;variant?: 'h1' | 'body' | 'caption';children: React.ReactNode;} & ComponentPropsWithoutRef<T>; // ๐ Inherit valid props for that tag (e.g., href for <a>)export const Text = <T extends ElementType = 'span'>({as,variant = 'body',children,...props}: TextProps<T>) => {// If 'as' is not provided, default to 'span'const Component = as || 'span';// Dynamic Tailwind classes based on variantconst styles = {h1: "text-4xl font-bold tracking-tight",body: "text-base text-gray-700",caption: "text-xs text-gray-500 uppercase"};return (<Component className={styles[variant]} {...props}>{children}</Component>);};// Usage Examples:// 1. Renders as an <h1><Text as="h1" variant="h1">Page Title</Text>// 2. Renders as a <p><Text as="p" variant="body">Some description...</Text>// 3. Renders as an <label> (TS knows 'htmlFor' is valid here!)<Text as="label" htmlFor="email" variant="caption">Email Address</Text>// Advanced: Polymorphic Button Componenttype ButtonProps<T extends ElementType> = {as?: T;variant?: 'primary' | 'secondary' | 'danger';size?: 'sm' | 'md' | 'lg';children: React.ReactNode;} & ComponentPropsWithoutRef<T>;export const Button = <T extends ElementType = 'button'>({as,variant = 'primary',size = 'md',children,...props}: ButtonProps<T>) => {const Component = as || 'button';const baseStyles = "font-semibold rounded-lg transition-colors";const variants = {primary: "bg-blue-600 text-white hover:bg-blue-700",secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",danger: "bg-red-600 text-white hover:bg-red-700"};const sizes = {sm: "px-3 py-1.5 text-sm",md: "px-4 py-2 text-base",lg: "px-6 py-3 text-lg"};return (<ComponentclassName={`${baseStyles} ${variants[variant]} ${sizes[size]}`}{...props}>{children}</Component>);};// Usage: Same component, different HTML elements// As a button<Button onClick={() => console.log('clicked')}>Click Me</Button>// As a link (TypeScript knows 'href' is valid!)<Button as="a" href="/dashboard">Go to Dashboard</Button>// As a div (for custom click handlers)<Button as="div" role="button" tabIndex={0}>Custom Element</Button>// Advanced: Polymorphic Card Componenttype CardProps<T extends ElementType> = {as?: T;children: React.ReactNode;} & ComponentPropsWithoutRef<T>;export const Card = <T extends ElementType = 'div'>({as,children,...props}: CardProps<T>) => {const Component = as || 'div';return (<ComponentclassName="bg-white rounded-lg shadow-md p-6"{...props}>{children}</Component>);};// Usage:// As a div<Card>Regular card content</Card>// As an article (semantic HTML)<Card as="article"><h2>Article Title</h2><p>Article content...</p></Card>// As a link (clickable card)<Card as="a" href="/post/123"><h3>Post Title</h3><p>Post excerpt...</p></Card>
๐ Summary of Senior Practices Used
๐ข Impact: LOW โ A handy recap of the power moves you just learned โ bookmark this for your next code review! ๐
๐ In this section: Generics Recap โข Composition Patterns โข Prop Spreading โข Inversion of Control
1. Generics (`<T>`): Used in Render Props and Polymorphic components to ensure type safety, no matter what data or tag is passed. This allows components to be truly reusable while maintaining full TypeScript support.
2. Composition: Used in Wrapper Components to separate "Access Logic" from "UI Logic". This pattern enables building complex permission systems and feature flags without cluttering your components with conditional logic.
3. Prop Spreading (`...props`): Used in Polymorphic components to pass native attributes (like onClick or href) down to the underlying DOM node without manually defining them. This ensures full compatibility with native HTML element props.
4. Inversion of Control: Render Props pattern gives consumers complete control over rendering while the component handles the logic. This is the foundation of headless UI libraries.