UI Component Patterns
When to use this skill
-
Building Component Libraries: Creating reusable UI components
-
Implementing Design Systems: Applying consistent UI patterns
-
Complex UI: Components requiring multiple variants (Button, Modal, Dropdown)
-
Refactoring: Extracting duplicate code into components
Instructions
Step 1: Props API Design
Design Props that are easy to use and extensible.
Principles:
-
Clear names
-
Reasonable defaults
-
Type definitions with TypeScript
-
Optional Props use optional marker (?)
Example (Button):
interface ButtonProps { // Required children: React.ReactNode;
// Optional (with defaults) variant?: 'primary' | 'secondary' | 'outline' | 'ghost'; size?: 'sm' | 'md' | 'lg'; disabled?: boolean; isLoading?: boolean;
// Event handlers onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
// HTML attribute inheritance type?: 'button' | 'submit' | 'reset'; className?: string; }
function Button({
children,
variant = 'primary',
size = 'md',
disabled = false,
isLoading = false,
onClick,
type = 'button',
className = '',
...rest
}: ButtonProps) {
const baseClasses = 'btn';
const variantClasses = btn-${variant};
const sizeClasses = btn-${size};
const classes = ${baseClasses} ${variantClasses} ${sizeClasses} ${className};
return ( <button type={type} className={classes} disabled={disabled || isLoading} onClick={onClick} {...rest} > {isLoading ? <Spinner /> : children} </button> ); }
// Usage example <Button variant="primary" size="lg" onClick={() => alert('Clicked!')}> Click Me </Button>
Step 2: Composition Pattern
Combine small components to build complex UI.
Example (Card):
// Card component (Container) interface CardProps { children: React.ReactNode; className?: string; }
function Card({ children, className = '' }: CardProps) {
return <div className={card ${className}}>{children}</div>;
}
// Card.Header function CardHeader({ children }: { children: React.ReactNode }) { return <div className="card-header">{children}</div>; }
// Card.Body function CardBody({ children }: { children: React.ReactNode }) { return <div className="card-body">{children}</div>; }
// Card.Footer function CardFooter({ children }: { children: React.ReactNode }) { return <div className="card-footer">{children}</div>; }
// Compound Component pattern Card.Header = CardHeader; Card.Body = CardBody; Card.Footer = CardFooter;
export default Card;
// Usage import Card from './Card';
function ProductCard() { return ( <Card> <Card.Header> <h3>Product Name</h3> </Card.Header> <Card.Body> <img src="..." alt="Product" /> <p>Product description here...</p> </Card.Body> <Card.Footer> <button>Add to Cart</button> </Card.Footer> </Card> ); }
Step 3: Render Props / Children as Function
A pattern for flexible customization.
Example (Dropdown):
interface DropdownProps<T> { items: T[]; renderItem: (item: T, index: number) => React.ReactNode; onSelect: (item: T) => void; placeholder?: string; }
function Dropdown<T>({ items, renderItem, onSelect, placeholder }: DropdownProps<T>) { const [isOpen, setIsOpen] = useState(false); const [selected, setSelected] = useState<T | null>(null);
const handleSelect = (item: T) => { setSelected(item); onSelect(item); setIsOpen(false); };
return ( <div className="dropdown"> <button onClick={() => setIsOpen(!isOpen)}> {selected ? renderItem(selected, -1) : placeholder || 'Select...'} </button>
{isOpen && (
<ul className="dropdown-menu">
{items.map((item, index) => (
<li key={index} onClick={() => handleSelect(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
)}
</div>
); }
// Usage interface User { id: string; name: string; avatar: string; }
function UserDropdown() { const users: User[] = [...];
return ( <Dropdown items={users} placeholder="Select a user" renderItem={(user) => ( <div className="user-item"> <img src={user.avatar} alt={user.name} /> <span>{user.name}</span> </div> )} onSelect={(user) => console.log('Selected:', user)} /> ); }
Step 4: Separating Logic with Custom Hooks
Separate UI from business logic.
Example (Modal):
// hooks/useModal.ts function useModal(initialOpen = false) { const [isOpen, setIsOpen] = useState(initialOpen);
const open = useCallback(() => setIsOpen(true), []); const close = useCallback(() => setIsOpen(false), []); const toggle = useCallback(() => setIsOpen(prev => !prev), []);
return { isOpen, open, close, toggle }; }
// components/Modal.tsx interface ModalProps { isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode; }
function Modal({ isOpen, onClose, title, children }: ModalProps) { if (!isOpen) return null;
return ( <div className="modal-overlay" onClick={onClose}> <div className="modal-content" onClick={(e) => e.stopPropagation()}> <div className="modal-header"> <h2>{title}</h2> <button onClick={onClose} aria-label="Close">×</button> </div> <div className="modal-body">{children}</div> </div> </div> ); }
// Usage function App() { const { isOpen, open, close } = useModal();
return ( <> <button onClick={open}>Open Modal</button> <Modal isOpen={isOpen} onClose={close} title="My Modal"> <p>Modal content here...</p> </Modal> </> ); }
Step 5: Performance Optimization
Prevent unnecessary re-renders.
React.memo:
// ❌ Bad: child re-renders every time parent re-renders function ExpensiveComponent({ data }) { console.log('Rendering...'); return <div>{/* Complex UI */}</div>; }
// ✅ Good: re-renders only when props change const ExpensiveComponent = React.memo(({ data }) => { console.log('Rendering...'); return <div>{/* Complex UI */}</div>; });
useMemo & useCallback:
function ProductList({ products, category }: { products: Product[]; category: string }) { // ✅ Memoize filtered results const filteredProducts = useMemo(() => { return products.filter(p => p.category === category); }, [products, category]);
// ✅ Memoize callback const handleAddToCart = useCallback((productId: string) => { // Add to cart console.log('Adding:', productId); }, []);
return ( <div> {filteredProducts.map(product => ( <ProductCard key={product.id} product={product} onAddToCart={handleAddToCart} /> ))} </div> ); }
const ProductCard = React.memo(({ product, onAddToCart }) => { return ( <div> <h3>{product.name}</h3> <button onClick={() => onAddToCart(product.id)}>Add to Cart</button> </div> ); });
Output format
Component File Structure
components/ ├── Button/ │ ├── Button.tsx # Main component │ ├── Button.test.tsx # Tests │ ├── Button.stories.tsx # Storybook │ ├── Button.module.css # Styles │ └── index.ts # Export ├── Card/ │ ├── Card.tsx │ ├── CardHeader.tsx │ ├── CardBody.tsx │ ├── CardFooter.tsx │ └── index.ts └── Modal/ ├── Modal.tsx ├── useModal.ts # Custom hook └── index.ts
Component Template
import React from 'react';
export interface ComponentProps { // Props definition children: React.ReactNode; className?: string; }
/**
- Component description
- @example
-
- <Component>Hello</Component>
-
*/
export const Component = React.forwardRef<HTMLDivElement, ComponentProps>(
({ children, className = '', ...rest }, ref) => {
return (
<div ref={ref} className={component ${className}} {...rest}>
{children}
</div>
);
}
);
Component.displayName = 'Component';
export default Component;
Constraints
Required Rules (MUST)
Single Responsibility Principle: One component has one role only
-
Button handles buttons only, Form handles forms only
Props Type Definition: TypeScript interface required
-
Enables auto-completion
-
Type safety
Accessibility: aria-*, role, tabindex, etc.
Prohibited Rules (MUST NOT)
Excessive props drilling: Prohibited when 5+ levels deep
-
Use Context or Composition
No Business Logic: Prohibit API calls and complex calculations in UI components
-
Separate into custom hooks
Inline objects/functions: Performance degradation
// ❌ Bad example <Component style={{ color: 'red' }} onClick={() => handleClick()} />
// ✅ Good example const style = { color: 'red' }; const handleClick = useCallback(() => {...}, []); <Component style={style} onClick={handleClick} />
Examples
Example 1: Accordion (Compound Component)
import React, { createContext, useContext, useState } from 'react';
// Share state with Context const AccordionContext = createContext<{ activeIndex: number | null; setActiveIndex: (index: number | null) => void; } | null>(null);
function Accordion({ children }: { children: React.ReactNode }) { const [activeIndex, setActiveIndex] = useState<number | null>(null);
return ( <AccordionContext.Provider value={{ activeIndex, setActiveIndex }}> <div className="accordion">{children}</div> </AccordionContext.Provider> ); }
function AccordionItem({ index, title, children }: { index: number; title: string; children: React.ReactNode; }) { const context = useContext(AccordionContext); if (!context) throw new Error('AccordionItem must be used within Accordion');
const { activeIndex, setActiveIndex } = context; const isActive = activeIndex === index;
return ( <div className="accordion-item"> <button className="accordion-header" onClick={() => setActiveIndex(isActive ? null : index)} aria-expanded={isActive} > {title} </button> {isActive && <div className="accordion-body">{children}</div>} </div> ); }
Accordion.Item = AccordionItem; export default Accordion;
// Usage <Accordion> <Accordion.Item index={0} title="Section 1"> Content for section 1 </Accordion.Item> <Accordion.Item index={1} title="Section 2"> Content for section 2 </Accordion.Item> </Accordion>
Example 2: Polymorphic Component (as prop)
type PolymorphicComponentProps<C extends React.ElementType> = { as?: C; children: React.ReactNode; } & React.ComponentPropsWithoutRef<C>;
function Text<C extends React.ElementType = 'span'>({ as, children, ...rest }: PolymorphicComponentProps<C>) { const Component = as || 'span'; return <Component {...rest}>{children}</Component>; }
// Usage <Text>Default span</Text> <Text as="h1">Heading 1</Text> <Text as="p" style={{ color: 'blue' }}>Paragraph</Text> <Text as={Link} href="/about">Link</Text>
Best practices
-
Composition over Props: Leverage children instead of many props
-
Controlled vs Uncontrolled: Choose based on situation
-
Default Props: Provide reasonable defaults
-
Storybook: Component documentation and development
References
-
React Patterns
-
Compound Components
-
Radix UI - Accessible components
-
Chakra UI - Component library
-
shadcn/ui - Copy-paste components
Metadata
Version
-
Current Version: 1.0.0
-
Last Updated: 2025-01-01
-
Compatible Platforms: Claude, ChatGPT, Gemini
Related Skills
-
web-accessibility: Accessible components
-
state-management: Component state management
Tags
#UI-components #React #design-patterns #composition #TypeScript #frontend