Thời lượng: 1 tuần
Level: Intermediate
Yêu cầu: Modules 1-3 hoàn thành, React cơ bản
Tổng quan Module
Module này dạy bạn cách thiết kế component API tốt, chọn giữa composition vs configuration, và implement compound components pattern cho flexibility.
Kết quả học tập
Sau module này, bạn sẽ:
- Thiết kế component API dễ sử dụng
- Áp dụng composition patterns
- Implement compound components
- Avoid prop drilling và API pollution
Bài 4.1: Component API Design
Lý thuyết
Good Component API:
// Bad: Too many props, unclear purpose
<Button
color="blue"
size="medium"
rounded={true}
shadow={true}
loading={false}
disabled={false}
fullWidth={false}
/>
// Good: Clear variants, sensible defaults
<Button variant="primary" size="md">
Click me
</Button>API Design Principles:
-
Sensible Defaults
- Most common use case = zero config
-
Progressive Disclosure
- Simple things easy, complex things possible
-
Consistency
- Same pattern across all components
-
Type Safety
- TypeScript makes invalid states impossible
Nguyên tắc then chốt
"API tốt là API không cần documentation để hiểu. Tên prop tự giải thích."
Thực hành
Bài tập: Design Button component API
Requirements:
- 3 variants:
primary,secondary,ghost - 3 sizes:
sm,md,lg - Optional:
disabled,loading,fullWidth - Support:
onClick,type(submit/button/reset)
interface ButtonProps {
/** Visual variant */
variant?: 'primary' | 'secondary' | 'ghost';
/** Size */
size?: 'sm' | 'md' | 'lg';
/** Disabled state */
disabled?: boolean;
/** Loading state with spinner */
loading?: boolean;
/** Full width */
fullWidth?: boolean;
/** Button type */
type?: 'button' | 'submit' | 'reset';
/** Click handler */
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
/** Children */
children: React.ReactNode;
}
const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
fullWidth = false,
type = 'button',
onClick,
children
}) => {
const classNames = [
'button',
`button--${variant}`,
`button--${size}`,
fullWidth && 'button--full-width',
loading && 'button--loading'
].filter(Boolean).join(' ');
return (
<button
type={type}
className={classNames}
disabled={disabled || loading}
onClick={onClick}
>
{loading && <Spinner size={size} />}
{children}
</button>
);
};CSS:
.button {
font-family: var(--font-sans);
font-weight: 600;
border-radius: var(--radius-md);
transition: var(--transition-base);
cursor: pointer;
}
/* Variants */
.button--primary {
background: var(--color-primary);
color: white;
border: none;
}
.button--primary:hover:not(:disabled) {
background: var(--color-primary-hover);
}
.button--secondary {
background: transparent;
color: var(--color-primary);
border: 2px solid var(--color-primary);
}
.button--ghost {
background: transparent;
color: var(--color-text-primary);
border: none;
}
/* Sizes */
.button--sm {
padding: var(--space-2) var(--space-4);
font-size: var(--font-size-sm);
}
.button--md {
padding: var(--space-3) var(--space-6);
font-size: var(--font-size-base);
}
.button--lg {
padding: var(--space-4) var(--space-8);
font-size: var(--font-size-lg);
}
/* States */
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.button--full-width {
width: 100%;
}
### Demo: Button Component
Trải nghiệm Button component với các tùy chọn khác nhau:
```playground-config
{
"controls": [
{
"name": "variant",
"label": "Biến thể",
"type": "select",
"options": ["primary", "secondary", "ghost"],
"default": "primary"
},
{
"name": "size",
"label": "Kích thước",
"type": "select",
"options": ["sm", "md", "lg"],
"default": "md"
},
{
"name": "text",
"label": "Nội dung",
"type": "text",
"default": "Click me"
},
{
"name": "radius",
"label": "Bo góc",
"type": "range",
"min": 0,
"max": 24,
"step": 2,
"default": 8
}
]
}Nhiệm vụ:
- Implement Button với 3 variants
- Add loading state với spinner
- Support all sizes
- Write Storybook stories
Câu hỏi thảo luận
- Boolean Props:
<Button primary />vs<Button variant="primary" />? Pros/cons? - Polymorphic Components: Button có nên render
<a>nếu cóhrefkhông? - Icon Buttons:
<Button icon={<StarIcon />} />hay<IconButton icon={<StarIcon />} />?
Bài 4.2: Composition vs Configuration
Lý thuyết
Configuration (Props-heavy):
<Card
title="Product"
subtitle="Description"
image="/product.jpg"
actions={[
{ label: "Edit", onClick: handleEdit },
{ label: "Delete", onClick: handleDelete }
]}
/>Composition (Slots):
<Card>
<Card.Image src="/product.jpg" />
<Card.Header>
<Card.Title>Product</Card.Title>
<Card.Subtitle>Description</Card.Subtitle>
</Card.Header>
<Card.Actions>
<Button onClick={handleEdit}>Edit</Button>
<Button onClick={handleDelete}>Delete</Button>
</Card.Actions>
</Card>When to use each:
- Configuration: Simple, limited variants
- Composition: Complex, many variants
Nguyên tắc then chốt
"Composition over configuration khi có nhiều hơn 5 props. Props không scale."
Thực hành
Bài tập: Convert Modal từ configuration sang composition
Before (Configuration):
<Modal
title="Delete Item"
content="Are you sure?"
confirmText="Delete"
cancelText="Cancel"
onConfirm={handleDelete}
onCancel={handleCancel}
variant="danger"
/>After (Composition):
<Modal open={open} onClose={handleCancel}>
<Modal.Header>
<Modal.Title>Delete Item</Modal.Title>
</Modal.Header>
<Modal.Body>
<Text>Are you sure you want to delete this item?</Text>
</Modal.Body>
<Modal.Footer>
<Button variant="ghost" onClick={handleCancel}>
Cancel
</Button>
<Button variant="danger" onClick={handleDelete}>
Delete
</Button>
</Modal.Footer>
</Modal>Implementation:
interface ModalProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
}
const Modal: React.FC<ModalProps> & {
Header: typeof ModalHeader;
Title: typeof ModalTitle;
Body: typeof ModalBody;
Footer: typeof ModalFooter;
} = ({ open, onClose, children }) => {
if (!open) return null;
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
);
};
const ModalHeader: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div className="modal-header">{children}</div>
);
const ModalTitle: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<h2 className="modal-title">{children}</h2>
);
const ModalBody: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div className="modal-body">{children}</div>
);
const ModalFooter: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div className="modal-footer">{children}</div>
);
Modal.Header = ModalHeader;
Modal.Title = ModalTitle;
Modal.Body = ModalBody;
Modal.Footer = ModalFooter;Nhiệm vụ:
- Implement Modal với compound components
- Support close button trong Header
- Add transitions (fade in/out)
- Handle focus trap và escape key
Câu hỏi thảo luận
- Learning Curve: Composition khó học hơn configuration. Worth it không?
- Required Parts: Nếu Modal.Footer bắt buộc, làm sao enforce?
- Flexibility vs Consistency: Too much flexibility → inconsistent UIs?
Bài 4.3: Compound Components Pattern
Lý thuyết
Compound Components:
- Parent component share state với children qua Context
- Children components biết cách interact với parent
- Flexible nhưng vẫn type-safe
Example: Tabs
<Tabs defaultValue="account">
<Tabs.List>
<Tabs.Trigger value="account">Account</Tabs.Trigger>
<Tabs.Trigger value="password">Password</Tabs.Trigger>
</Tabs.List>
<Tabs.Panel value="account">
<AccountSettings />
</Tabs.Panel>
<Tabs.Panel value="password">
<PasswordSettings />
</Tabs.Panel>
</Tabs>Nguyên tắc then chốt
"Compound components chia sẻ state ngầm qua Context, không qua props drilling."
Thực hành
Bài tập: Implement Accordion với compound components
<Accordion>
<Accordion.Item value="item-1">
<Accordion.Trigger>
What is Design System?
</Accordion.Trigger>
<Accordion.Content>
A collection of reusable components...
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-2">
<Accordion.Trigger>
Why use Design Tokens?
</Accordion.Trigger>
<Accordion.Content>
Tokens ensure consistency...
</Accordion.Content>
</Accordion.Item>
</Accordion>Implementation:
interface AccordionContextValue {
openItems: Set<string>;
toggle: (value: string) => void;
}
const AccordionContext = React.createContext<AccordionContextValue | null>(null);
const Accordion: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [openItems, setOpenItems] = React.useState<Set<string>>(new Set());
const toggle = (value: string) => {
setOpenItems(prev => {
const next = new Set(prev);
if (next.has(value)) {
next.delete(value);
} else {
next.add(value);
}
return next;
});
};
return (
<AccordionContext.Provider value={{ openItems, toggle }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
};
const AccordionItem: React.FC<{ value: string; children: React.ReactNode }> = ({
value,
children
}) => (
<div className="accordion-item" data-value={value}>
{children}
</div>
);
const AccordionTrigger: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const context = React.useContext(AccordionContext);
if (!context) throw new Error('AccordionTrigger must be inside Accordion');
// Get value from parent AccordionItem
const itemElement = React.useRef<HTMLDivElement>(null);
const value = itemElement.current?.closest('[data-value]')?.getAttribute('data-value') || '';
const isOpen = context.openItems.has(value);
return (
<button
className="accordion-trigger"
onClick={() => context.toggle(value)}
aria-expanded={isOpen}
>
{children}
<ChevronIcon className={isOpen ? 'rotate-180' : ''} />
</button>
);
};
const AccordionContent: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const context = React.useContext(AccordionContext);
if (!context) throw new Error('AccordionContent must be inside Accordion');
const itemElement = React.useRef<HTMLDivElement>(null);
const value = itemElement.current?.closest('[data-value]')?.getAttribute('data-value') || '';
const isOpen = context.openItems.has(value);
return (
<div className={`accordion-content ${isOpen ? 'open' : ''}`}>
{children}
</div>
);
};
Accordion.Item = AccordionItem;
Accordion.Trigger = AccordionTrigger;
Accordion.Content = AccordionContent;Nhiệm vụ:
- Implement Accordion
- Support controlled mode (
valueprop) - Add animations (height transition)
- Support multiple mode (nhiều items mở cùng lúc)
Câu hỏi thảo luận
- Context Performance: Re-render toàn bộ tree khi state change? Optimize thế nào?
- TypeScript: Làm sao enforce "Trigger phải có Content matching"?
- Accessibility: ARIA attributes nào cần cho Accordion?
Tổng kết Module
Bạn đã học:
- Component API design với sensible defaults
- Composition vs Configuration trade-offs
- Compound Components pattern với Context
Bước tiếp theo
Module 5 sẽ dạy về Form Components - inputs, validation, và error handling.