Appearance
组件设计原则 - React组件架构最佳实践
本文档详细阐述React组件设计的核心原则与最佳实践,包括组件拆分、职责划分、复用性设计、可测试性等方面的指导。
1. SOLID原则在React中的应用
1.1 单一职责原则 (Single Responsibility Principle)
原则: 一个组件应该只有一个改变的理由
tsx
// ❌ 违反单一职责: 组件做了太多事情
function UserDashboard() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [notifications, setNotifications] = useState([]);
const [settings, setSettings] = useState({});
useEffect(() => {
// 获取用户信息
fetchUser().then(setUser);
// 获取帖子
fetchPosts().then(setPosts);
// 获取通知
fetchNotifications().then(setNotifications);
// 获取设置
fetchSettings().then(setSettings);
}, []);
const handleLogout = () => { /* ... */ };
const handleUpdateProfile = () => { /* ... */ };
const handleDeletePost = () => { /* ... */ };
const handleMarkAsRead = () => { /* ... */ };
return (
<div>
<UserProfile user={user} onUpdate={handleUpdateProfile} onLogout={handleLogout} />
<PostList posts={posts} onDelete={handleDeletePost} />
<NotificationList notifications={notifications} onMarkAsRead={handleMarkAsRead} />
<Settings settings={settings} />
</div>
);
}
// ✅ 遵循单一职责: 拆分为多个职责单一的组件
function UserDashboard() {
return (
<div>
<UserProfileSection />
<PostListSection />
<NotificationSection />
<SettingsSection />
</div>
);
}
function UserProfileSection() {
const { user, loading, error, updateProfile, logout } = useUserProfile();
if (loading) return <Skeleton />;
if (error) return <Error error={error} />;
return <UserProfile user={user} onUpdate={updateProfile} onLogout={logout} />;
}
function PostListSection() {
const { posts, loading, deletePost } = usePosts();
if (loading) return <Skeleton />;
return <PostList posts={posts} onDelete={deletePost} />;
}
function NotificationSection() {
const { notifications, markAsRead } = useNotifications();
return <NotificationList notifications={notifications} onMarkAsRead={markAsRead} />;
}
function SettingsSection() {
const { settings, updateSettings } = useSettings();
return <Settings settings={settings} onUpdate={updateSettings} />;
}1.2 开闭原则 (Open/Closed Principle)
原则: 对扩展开放,对修改关闭
tsx
// ❌ 违反开闭原则: 添加新类型需要修改组件
function Alert({ type, message }: { type: string; message: string }) {
let icon;
let color;
if (type === 'success') {
icon = <CheckIcon />;
color = 'green';
} else if (type === 'error') {
icon = <ErrorIcon />;
color = 'red';
} else if (type === 'warning') {
icon = <WarningIcon />;
color = 'yellow';
} else if (type === 'info') {
icon = <InfoIcon />;
color = 'blue';
}
return (
<div style={{ color }}>
{icon}
<span>{message}</span>
</div>
);
}
// ✅ 遵循开闭原则: 使用配置扩展
const ALERT_CONFIGS = {
success: { icon: CheckIcon, color: 'green' },
error: { icon: ErrorIcon, color: 'red' },
warning: { icon: WarningIcon, color: 'yellow' },
info: { icon: InfoIcon, color: 'blue' }
};
function Alert({ type, message }: { type: keyof typeof ALERT_CONFIGS; message: string }) {
const config = ALERT_CONFIGS[type];
const Icon = config.icon;
return (
<div style={{ color: config.color }}>
<Icon />
<span>{message}</span>
</div>
);
}
// ✅ 更好: 通过props扩展
interface AlertProps {
icon: React.ComponentType;
color: string;
message: string;
}
function Alert({ icon: Icon, color, message }: AlertProps) {
return (
<div style={{ color }}>
<Icon />
<span>{message}</span>
</div>
);
}
// 使用
<Alert icon={CheckIcon} color="green" message="Success!" />1.3 里氏替换原则 (Liskov Substitution Principle)
原则: 子组件应该可以替换父组件而不影响程序正确性
tsx
// 基础Button组件
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}
function Button({ children, onClick, disabled }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled}>
{children}
</button>
);
}
// ✅ IconButton继承Button的所有行为
interface IconButtonProps extends ButtonProps {
icon: React.ComponentType;
}
function IconButton({ icon: Icon, children, ...props }: IconButtonProps) {
return (
<Button {...props}>
<Icon />
{children}
</Button>
);
}
// ✅ LoadingButton也完全兼容Button
interface LoadingButtonProps extends ButtonProps {
loading?: boolean;
}
function LoadingButton({ loading, children, disabled, ...props }: LoadingButtonProps) {
return (
<Button {...props} disabled={disabled || loading}>
{loading ? <Spinner /> : children}
</Button>
);
}
// 任何使用Button的地方都可以替换为IconButton或LoadingButton
function Form() {
return (
<div>
<Button onClick={() => {}}>Submit</Button>
<IconButton icon={SaveIcon} onClick={() => {}}>Save</IconButton>
<LoadingButton loading={true} onClick={() => {}}>Loading...</LoadingButton>
</div>
);
}1.4 接口隔离原则 (Interface Segregation Principle)
原则: 不应该强迫组件依赖它不需要的props
tsx
// ❌ 违反接口隔离: 过多的props
interface UserCardProps {
user: User;
onEdit: () => void;
onDelete: () => void;
onFollow: () => void;
onUnfollow: () => void;
onMessage: () => void;
onBlock: () => void;
onReport: () => void;
showEditButton: boolean;
showDeleteButton: boolean;
showFollowButton: boolean;
showMessageButton: boolean;
}
// ✅ 遵循接口隔离: 拆分为更小的接口
interface BaseUserCardProps {
user: User;
}
interface UserCardWithActionsProps extends BaseUserCardProps {
actions?: {
onEdit?: () => void;
onDelete?: () => void;
onFollow?: () => void;
onMessage?: () => void;
};
}
function UserCard({ user, actions }: UserCardWithActionsProps) {
return (
<div>
<UserAvatar src={user.avatar} />
<UserName>{user.name}</UserName>
{actions && <UserActions actions={actions} />}
</div>
);
}
// 或者使用组合
function UserCard({ user, children }: BaseUserCardProps & { children?: React.ReactNode }) {
return (
<div>
<UserAvatar src={user.avatar} />
<UserName>{user.name}</UserName>
{children}
</div>
);
}
// 使用
<UserCard user={user}>
<Button onClick={onEdit}>Edit</Button>
<Button onClick={onDelete}>Delete</Button>
</UserCard>1.5 依赖倒置原则 (Dependency Inversion Principle)
原则: 依赖抽象而不是具体实现
tsx
// ❌ 违反依赖倒置: 直接依赖具体实现
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
// 直接依赖fetch实现
fetch('/api/users')
.then(res => res.json())
.then(setUsers);
}, []);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// ✅ 遵循依赖倒置: 依赖抽象接口
interface DataFetcher<T> {
fetch: () => Promise<T>;
}
interface UserListProps {
fetcher: DataFetcher<User[]>;
}
function UserList({ fetcher }: UserListProps) {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
fetcher.fetch().then(setUsers);
}, [fetcher]);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// 使用
const apiFetcher: DataFetcher<User[]> = {
fetch: () => fetch('/api/users').then(res => res.json())
};
<UserList fetcher={apiFetcher} />
// 或者使用Hook抽象
function useUsers() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUsers().then(data => {
setUsers(data);
setLoading(false);
});
}, []);
return { users, loading };
}
function UserList() {
const { users, loading } = useUsers();
if (loading) return <Skeleton />;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}2. 组件设计模式
2.1 容器组件与展示组件
tsx
// 展示组件 (Presentational Component)
// 职责: 只负责UI渲染
// 特点: 无状态、通过props接收数据、纯函数
interface ProductCardProps {
product: Product;
onAddToCart: (product: Product) => void;
onViewDetails: (id: string) => void;
}
function ProductCard({ product, onAddToCart, onViewDetails }: ProductCardProps) {
return (
<div>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.price}</p>
<button onClick={() => onAddToCart(product)}>Add to Cart</button>
<button onClick={() => onViewDetails(product.id)}>Details</button>
</div>
);
}
// 容器组件 (Container Component)
// 职责: 处理数据和业务逻辑
// 特点: 有状态、调用API、传递数据给展示组件
function ProductCardContainer({ productId }: { productId: string }) {
const { product, loading, error } = useProduct(productId);
const { addToCart } = useCart();
const navigate = useNavigate();
if (loading) return <Skeleton />;
if (error) return <Error error={error} />;
if (!product) return null;
const handleAddToCart = (product: Product) => {
addToCart(product);
toast.success('Added to cart!');
};
const handleViewDetails = (id: string) => {
navigate(`/products/${id}`);
};
return (
<ProductCard
product={product}
onAddToCart={handleAddToCart}
onViewDetails={handleViewDetails}
/>
);
}2.2 复合组件模式 (Compound Components)
tsx
// 使用React Context实现复合组件
const TabsContext = createContext<{
activeTab: string;
setActiveTab: (tab: string) => void;
} | null>(null);
function Tabs({ children, defaultTab }: { children: React.ReactNode; defaultTab: string }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div>{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }: { children: React.ReactNode }) {
return <div role="tablist">{children}</div>;
}
function Tab({ id, children }: { id: string; children: React.ReactNode }) {
const context = useContext(TabsContext);
if (!context) throw new Error('Tab must be used within Tabs');
const { activeTab, setActiveTab } = context;
return (
<button
role="tab"
aria-selected={activeTab === id}
onClick={() => setActiveTab(id)}
>
{children}
</button>
);
}
function TabPanels({ children }: { children: React.ReactNode }) {
return <div>{children}</div>;
}
function TabPanel({ id, children }: { id: string; children: React.ReactNode }) {
const context = useContext(TabsContext);
if (!context) throw new Error('TabPanel must be used within Tabs');
const { activeTab } = context;
if (activeTab !== id) return null;
return <div role="tabpanel">{children}</div>;
}
// 组合使用
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panels = TabPanels;
Tabs.Panel = TabPanel;
// 使用
function App() {
return (
<Tabs defaultTab="tab1">
<Tabs.List>
<Tabs.Tab id="tab1">Tab 1</Tabs.Tab>
<Tabs.Tab id="tab2">Tab 2</Tabs.Tab>
<Tabs.Tab id="tab3">Tab 3</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel id="tab1">Content 1</Tabs.Panel>
<Tabs.Panel id="tab2">Content 2</Tabs.Panel>
<Tabs.Panel id="tab3">Content 3</Tabs.Panel>
</Tabs.Panels>
</Tabs>
);
}2.3 Render Props模式
tsx
// Render Props组件
interface MouseTrackerProps {
render: (position: { x: number; y: number }) => React.ReactNode;
}
function MouseTracker({ render }: MouseTrackerProps) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return <>{render(position)}</>;
}
// 使用
function App() {
return (
<MouseTracker
render={({ x, y }) => (
<div>
Mouse position: ({x}, {y})
</div>
)}
/>
);
}
// 或者使用children作为函数
interface DataProviderProps<T> {
data: T;
children: (data: T) => React.ReactNode;
}
function DataProvider<T>({ data, children }: DataProviderProps<T>) {
return <>{children(data)}</>;
}
// 使用
<DataProvider data={users}>
{(users) => (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</DataProvider>2.4 高阶组件 (HOC)
tsx
// HOC: withAuth
function withAuth<P extends object>(Component: React.ComponentType<P>) {
return function AuthenticatedComponent(props: P) {
const { user, loading } = useAuth();
if (loading) {
return <Spinner />;
}
if (!user) {
return <Navigate to="/login" />;
}
return <Component {...props} />;
};
}
// HOC: withLoading
interface WithLoadingProps {
loading: boolean;
}
function withLoading<P extends object>(
Component: React.ComponentType<P>
) {
return function WithLoadingComponent(props: P & WithLoadingProps) {
const { loading, ...rest } = props;
if (loading) {
return <Spinner />;
}
return <Component {...(rest as P)} />;
};
}
// 使用
const AuthenticatedDashboard = withAuth(Dashboard);
const DashboardWithLoading = withLoading(Dashboard);
// 组合HOC
const EnhancedDashboard = withAuth(withLoading(Dashboard));2.5 Hooks组合模式
tsx
// 组合多个Hooks创建复杂逻辑
function useUserData(userId: string) {
const { user, loading: userLoading, error: userError } = useUser(userId);
const { posts, loading: postsLoading } = usePosts(userId);
const { followers } = useFollowers(userId);
const loading = userLoading || postsLoading;
const error = userError;
return {
user,
posts,
followers,
loading,
error,
hasData: !loading && !error && user !== null
};
}
// 使用组合Hook
function UserProfile({ userId }: { userId: string }) {
const { user, posts, followers, loading, error, hasData } = useUserData(userId);
if (loading) return <Skeleton />;
if (error) return <Error error={error} />;
if (!hasData) return <NotFound />;
return (
<div>
<UserInfo user={user} />
<PostList posts={posts} />
<FollowerList followers={followers} />
</div>
);
}3. 组件拆分策略
3.1 何时拆分组件
tsx
// ❌ 过大的组件 (应该拆分)
function ProductPage() {
// 数百行代码...
// 职责: 产品信息、评论、推荐、购物车等
return (
<div>
{/* 大量JSX */}
</div>
);
}
// ✅ 拆分后的组件
function ProductPage({ productId }: { productId: string }) {
return (
<div>
<ProductHeader productId={productId} />
<ProductDetails productId={productId} />
<ProductReviews productId={productId} />
<RecommendedProducts productId={productId} />
</div>
);
}3.2 拆分原则
typescript
const componentSplittingPrinciples = {
按职责拆分: '每个组件只负责一个功能',
按可复用性拆分: '提取可复用的UI元素',
按数据流拆分: '根据数据源和流向拆分',
按性能拆分: '隔离频繁更新的部分',
按测试拆分: '更小的组件更易测试',
避免过度拆分: '不要为了拆分而拆分'
};4. Props设计原则
4.1 Props命名
tsx
// ✅ 良好的props命名
interface ButtonProps {
// 布尔值使用is/has/can等前缀
isLoading?: boolean;
isDisabled?: boolean;
hasIcon?: boolean;
// 事件处理器使用on前缀
onClick?: () => void;
onSubmit?: (data: FormData) => void;
onChange?: (value: string) => void;
// 回调函数使用具体动词
onUserSelect?: (user: User) => void;
onItemDelete?: (id: string) => void;
// 内容使用children或具体名称
children?: React.ReactNode;
header?: React.ReactNode;
footer?: React.ReactNode;
// 样式相关使用className/style
className?: string;
style?: React.CSSProperties;
}4.2 Props解构
tsx
// ✅ 解构props提高可读性
function Button({
children,
onClick,
isLoading = false,
isDisabled = false,
className = '',
...rest
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={isDisabled || isLoading}
className={`btn ${className}`}
{...rest}
>
{isLoading ? <Spinner /> : children}
</button>
);
}4.3 Props传递
tsx
// ✅ 使用spread operator传递props
function CustomInput(props: InputProps) {
return (
<div>
<label>{props.label}</label>
<input {...props} />
</div>
);
}
// ✅ 选择性传递props
function CustomButton({
customProp,
...buttonProps
}: CustomButtonProps) {
// customProp不会传递给button
return <button {...buttonProps} />;
}5. 状态设计原则
5.1 状态归属
tsx
// 状态应该放在最近的公共父组件
function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
return (
<div>
<TodoForm onAdd={(todo) => setTodos([...todos, todo])} />
<TodoList todos={todos} onToggle={(id) => {/* ... */}} />
<TodoStats todos={todos} />
</div>
);
}5.2 派生状态
tsx
// ❌ 不好: 冗余状态
function UserList({ users }: { users: User[] }) {
const [filteredUsers, setFilteredUsers] = useState(users);
const [filter, setFilter] = useState('');
useEffect(() => {
setFilteredUsers(
users.filter(u => u.name.includes(filter))
);
}, [users, filter]);
// ...
}
// ✅ 好: 派生状态
function UserList({ users }: { users: User[] }) {
const [filter, setFilter] = useState('');
const filteredUsers = useMemo(
() => users.filter(u => u.name.includes(filter)),
[users, filter]
);
// ...
}6. 组件文档化
tsx
/**
* Button组件
*
* 通用按钮组件,支持多种样式和状态
*
* @example
* ```tsx
* <Button variant="primary" onClick={handleClick}>
* Click Me
* </Button>
* ```
*/
interface ButtonProps {
/** 按钮内容 */
children: React.ReactNode;
/** 按钮样式变体 */
variant?: 'primary' | 'secondary' | 'danger';
/** 是否显示加载状态 */
isLoading?: boolean;
/** 点击事件处理器 */
onClick?: () => void;
}
export function Button({
children,
variant = 'primary',
isLoading = false,
onClick
}: ButtonProps) {
// ...
}7. 总结
优秀的组件设计应该:
- 遵循SOLID原则: 单一职责、开闭、里氏替换、接口隔离、依赖倒置
- 应用设计模式: 容器/展示、复合组件、Render Props、HOC、Hooks组合
- 合理拆分: 按职责、复用性、数据流、性能需求拆分
- 规范Props: 清晰命名、合理解构、正确传递
- 优化状态: 状态归属明确、避免冗余、使用派生状态
- 完善文档: JSDoc注释、使用示例、类型定义
持续实践这些原则可以构建可维护、可扩展的React应用。