React学习教程,从入门到精通,React-常用组件设计实例详解24
目录
React学习教程,从入门到精通,React 常用组件设计实例详解(24)
📘 React 常用组件设计实例详解
适用 React 版本:18.x(函数组件 + Hooks)
一、按钮组件设计
✅ 语法知识点
props
传递配置(类型、尺寸、禁用、点击事件)- 使用
className
动态拼接样式 - 使用
children
渲染按钮内容 - 使用
forwardRef
支持外部 ref 控制 - 使用 TypeScript 定义 Props 接口(可选)
🧩 案例代码:Button 组件
import React, { forwardRef, ButtonHTMLAttributes } from 'react';
import './Button.css'; // 假设你有对应的 CSS 文件
// 定义按钮类型
type ButtonType = 'primary' | 'secondary' | 'danger' | 'link';
type ButtonSize = 'small' | 'medium' | 'large';
// Props 接口
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
type?: ButtonType;
size?: ButtonSize;
disabled?: boolean;
loading?: boolean;
block?: boolean; // 是否块级按钮
}
// 使用 forwardRef 使外部能获取按钮 DOM
const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
const {
type = 'primary',
size = 'medium',
disabled = false,
loading = false,
block = false,
children,
className = '',
...rest
} = props;
// 动态类名拼接
const classes = [
'btn',
`btn-${type}`,
`btn-${size}`,
disabled && 'btn-disabled',
loading && 'btn-loading',
block && 'btn-block',
className
].filter(Boolean).join(' ');
return (
<button
ref={ref}
className={classes}
disabled={disabled || loading}
{...rest}
>
{loading ? (
<span className="btn-loading-icon">⏳</span>
) : null}
{children}
</button>
);
});
Button.displayName = 'Button'; // 便于调试
export default Button;
📄 Button.css 示例
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn-primary { background: #007bff; color: white; }
.btn-secondary { background: #6c757d; color: white; }
.btn-danger { background: #dc3545; color: white; }
.btn-link { background: transparent; color: #007bff; text-decoration: underline; }
.btn-small { padding: 4px 8px; font-size: 12px; }
.btn-medium { padding: 8px 16px; font-size: 14px; }
.btn-large { padding: 12px 24px; font-size: 16px; }
.btn-disabled { opacity: 0.6; cursor: not-allowed; }
.btn-loading { opacity: 0.8; }
.btn-block { display: block; width: 100%; }
.btn-loading-icon { margin-right: 8px; }
二、模态对话框组件设计(Modal)
✅ 语法知识点
- 使用
Portal
渲染到 body,避免父级样式干扰 - 使用
useState
控制显示/隐藏 - 使用
useEffect
监听 ESC 键关闭 - 使用
Backdrop
遮罩层 + 点击遮罩关闭 - 支持自定义标题、内容、底部按钮
🧩 案例代码:Modal 组件
import React, { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
interface ModalProps {
visible: boolean;
title?: string;
onClose: () => void;
children?: React.ReactNode;
footer?: React.ReactNode;
}
const Modal: React.FC<ModalProps> = ({
visible,
title = '提示',
onClose,
children,
footer
}) => {
const modalRef = useRef<HTMLDivElement>(null);
// 监听 ESC 键关闭
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
if (visible) {
document.addEventListener('keydown', handleEsc);
}
return () => {
document.removeEventListener('keydown', handleEsc);
};
}, [visible, onClose]);
// 点击遮罩关闭
const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onClose();
}
};
if (!visible) return null;
return createPortal(
<div className="modal-backdrop" onClick={handleBackdropClick}>
<div className="modal" ref={modalRef} onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h3>{title}</h3>
<button onClick={onClose} className="modal-close-btn">×</button>
</div>
<div className="modal-body">{children}</div>
{footer && <div className="modal-footer">{footer}</div>}
</div>
</div>,
document.body
);
};
export default Modal;
📄 Modal.css
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background: white;
border-radius: 8px;
min-width: 300px;
max-width: 90%;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.modal-header {
padding: 16px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-close-btn {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
}
.modal-body {
padding: 16px;
}
.modal-footer {
padding: 16px;
border-top: 1px solid #eee;
text-align: right;
}
三、树形组件设计(Tree)
✅ 语法知识点
- 递归渲染树节点
- 使用
useState
管理展开/折叠状态 - 支持 checkbox 多选(可选)
- 支持自定义节点渲染函数
- 使用
key
优化性能
🧩 案例代码:Tree 组件
import React, { useState } from 'react';
interface TreeNode {
key: string;
title: string;
children?: TreeNode[];
disabled?: boolean;
}
interface TreeProps {
treeData: TreeNode[];
onSelect?: (node: TreeNode) => void;
checkable?: boolean;
defaultExpandedKeys?: string[];
}
const Tree: React.FC<TreeProps> = ({
treeData,
onSelect,
checkable = false,
defaultExpandedKeys = []
}) => {
const [expandedKeys, setExpandedKeys] = useState<Set<string>>(
new Set(defaultExpandedKeys)
);
const [checkedKeys, setCheckedKeys] = useState<Set<string>>(new Set());
const toggleExpand = (key: string) => {
const newSet = new Set(expandedKeys);
if (newSet.has(key)) {
newSet.delete(key);
} else {
newSet.add(key);
}
setExpandedKeys(newSet);
};
const toggleCheck = (key: string, checked: boolean, node: TreeNode) => {
const newSet = new Set(checkedKeys);
if (checked) {
newSet.add(key);
// 如果有子节点,递归选中(简单实现)
const addChildrenKeys = (nodes?: TreeNode[]) => {
nodes?.forEach(child => {
newSet.add(child.key);
addChildrenKeys(child.children);
});
};
addChildrenKeys(node.children);
} else {
newSet.delete(key);
// 递归取消子节点
const removeChildrenKeys = (nodes?: TreeNode[]) => {
nodes?.forEach(child => {
newSet.delete(child.key);
removeChildrenKeys(child.children);
});
};
removeChildrenKeys(node.children);
}
setCheckedKeys(newSet);
};
const renderTreeNode = (node: TreeNode) => {
const isExpanded = expandedKeys.has(node.key);
const isChecked = checkedKeys.has(node.key);
const hasChildren = node.children && node.children.length > 0;
return (
<div key={node.key} style={{ marginLeft: '20px' }}>
<div
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
onClick={() => onSelect?.(node)}
>
{checkable && (
<input
type="checkbox"
checked={isChecked}
onChange={(e) => toggleCheck(node.key, e.target.checked, node)}
disabled={node.disabled}
style={{ marginRight: '8px' }}
/>
)}
{hasChildren && (
<span
onClick={(e) => {
e.stopPropagation();
toggleExpand(node.key);
}}
style={{ marginRight: '4px', cursor: 'pointer' }}
>
{isExpanded ? '▼' : '▶'}
</span>
)}
<span style={{ color: node.disabled ? '#ccc' : 'inherit' }}>
{node.title}
</span>
</div>
{hasChildren && isExpanded && (
<div>
{node.children.map(child => renderTreeNode(child))}
</div>
)}
</div>
);
};
return (
<div className="tree">
{treeData.map(node => renderTreeNode(node))}
</div>
);
};
export default Tree;
四、表格及分页组件设计
✅ 语法知识点
- 表格头(Thead)与表格体(Tbody)分离
- 使用
map
渲染数据行 - 分页组件独立,通过
props
通信 - 支持排序、选择、自定义列渲染
- 使用
memo
优化性能
🧩 表格头组件(TableHeader)
import React from 'react';
interface TableHeaderProps {
columns: { key: string; title: string; sortable?: boolean }[];
onSort?: (key: string) => void;
sortKey?: string;
sortOrder?: 'asc' | 'desc';
}
const TableHeader: React.FC<TableHeaderProps> = ({
columns,
onSort,
sortKey,
sortOrder
}) => {
return (
<thead>
<tr>
{columns.map(col => (
<th key={col.key} style={{ cursor: col.sortable ? 'pointer' : 'default' }}>
<div onClick={() => col.sortable && onSort?.(col.key)}>
{col.title}
{col.sortable && sortKey === col.key && (
<span style={{ marginLeft: '4px' }}>
{sortOrder === 'asc' ? '↑' : '↓'}
</span>
)}
</div>
</th>
))}
</tr>
</thead>
);
};
export default TableHeader;
🧩 表格体组件(TableBody)
import React from 'react';
interface TableBodyProps<T> {
data: T[];
columns: { key: string; render?: (text: any, record: T) => React.ReactNode }[];
rowKey: (record: T) => string;
onRowClick?: (record: T) => void;
}
const TableBody = <T extends Record<string, any>>({
data,
columns,
rowKey,
onRowClick
}: TableBodyProps<T>) => {
return (
<tbody>
{data.map(record => (
<tr
key={rowKey(record)}
onClick={() => onRowClick?.(record)}
style={{ cursor: onRowClick ? 'pointer' : 'default' }}
>
{columns.map(col => (
<td key={col.key}>
{col.render
? col.render(record[col.key], record)
: record[col.key]}
</td>
))}
</tr>
))}
</tbody>
);
};
export default TableBody;
🧩 分页组件(Pagination)
import React from 'react';
interface PaginationProps {
current: number;
total: number;
pageSize: number;
onChange: (page: number) => void;
}
const Pagination: React.FC<PaginationProps> = ({
current,
total,
pageSize,
onChange
}) => {
const totalPages = Math.ceil(total / pageSize);
const maxVisiblePages = 5;
const startPage = Math.max(1, current - Math.floor(maxVisiblePages / 2));
const endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
const pages = [];
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return (
<div className="pagination">
<button
onClick={() => onChange(Math.max(1, current - 1))}
disabled={current === 1}
>
上一页
</button>
{pages.map(page => (
<button
key={page}
onClick={() => onChange(page)}
className={page === current ? 'active' : ''}
>
{page}
</button>
))}
<button
onClick={() => onChange(Math.min(totalPages, current + 1))}
disabled={current === totalPages}
>
下一页
</button>
<span style={{ marginLeft: '16px' }}>
共 {total} 条,{totalPages} 页
</span>
</div>
);
};
export default Pagination;
🧩 表格组件(Table)整合
import React, { useState } from 'react';
import TableHeader from './TableHeader';
import TableBody from './TableBody';
import Pagination from './Pagination';
import './Table.css';
interface Column<T> {
key: string;
title: string;
render?: (text: any, record: T) => React.ReactNode;
sortable?: boolean;
}
interface TableProps<T> {
columns: Column<T>[];
data: T[];
rowKey: (record: T) => string;
pagination?: {
current: number;
pageSize: number;
total: number;
onChange: (page: number) => void;
};
onRowClick?: (record: T) => void;
}
const Table = <T extends Record<string, any>>({
columns,
data,
rowKey,
pagination,
onRowClick
}: TableProps<T>) => {
const [sortKey, setSortKey] = useState<string | null>(null);
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
const handleSort = (key: string) => {
if (sortKey === key) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else {
setSortKey(key);
setSortOrder('asc');
}
// 实际排序逻辑应在父组件处理,此处仅演示交互
};
return (
<div className="table-container">
<table className="table">
<TableHeader
columns={columns}
onSort={handleSort}
sortKey={sortKey || ''}
sortOrder={sortOrder}
/>
<TableBody
data={data}
columns={columns}
rowKey={rowKey}
onRowClick={onRowClick}
/>
</table>
{pagination && (
<Pagination
current={pagination.current}
total={pagination.total}
pageSize={pagination.pageSize}
onChange={pagination.onChange}
/>
)}
</div>
);
};
export default Table;
五、综合示例一:树表联动
点击树节点,表格数据联动刷新
import React, { useState } from 'react';
import Tree from './Tree';
import Table from './Table';
const treeData = [
{
key: '1',
title: '部门A',
children: [
{ key: '1-1', title: '小组A1' },
{ key: '1-2', title: '小组A2' }
]
},
{
key: '2',
title: '部门B',
children: [
{ key: '2-1', title: '小组B1' }
]
}
];
// 模拟数据源
const mockData: Record<string, { name: string; age: number; dept: string }[]> = {
'1': [
{ name: '张三', age: 25, dept: '部门A' },
{ name: '李四', age: 28, dept: '部门A' }
],
'1-1': [
{ name: '王五', age: 22, dept: '小组A1' }
],
'1-2': [
{ name: '赵六', age: 30, dept: '小组A2' }
],
'2': [
{ name: '钱七', age: 35, dept: '部门B' }
],
'2-1': [
{ name: '孙八', age: 27, dept: '小组B1' }
]
};
const TreeTableLinkage = () => {
const [selectedKey, setSelectedKey] = useState<string>('1');
const [tableData, setTableData] = useState(mockData['1']);
const handleTreeNodeSelect = (node: any) => {
setSelectedKey(node.key);
setTableData(mockData[node.key] || []);
};
const columns = [
{ key: 'name', title: '姓名' },
{ key: 'age', title: '年龄' },
{ key: 'dept', title: '所属部门' }
];
return (
<div style={{ display: 'flex', gap: '24px', padding: '20px' }}>
<div style={{ width: '300px' }}>
<h3>组织架构</h3>
<Tree
treeData={treeData}
onSelect={handleTreeNodeSelect}
defaultExpandedKeys={['1', '2']}
/>
</div>
<div style={{ flex: 1 }}>
<h3>成员列表(当前:{selectedKey})</h3>
<Table
columns={columns}
data={tableData}
rowKey={(record) => record.name}
/>
</div>
</div>
);
};
export default TreeTableLinkage;
六、综合示例二:消息管理综合示例
包含:表格展示、分页、模态框新增、按钮操作
import React, { useState } from 'react';
import Table from './Table';
import Pagination from './Pagination';
import Modal from './Modal';
import Button from './Button';
interface Message {
id: string;
title: string;
content: string;
status: 'read' | 'unread';
createdAt: string;
}
const initialMessages: Message[] = Array.from({ length: 50 }, (_, i) => ({
id: `msg-${i + 1}`,
title: `消息标题 ${i + 1}`,
content: `这是第 ${i + 1} 条消息的内容`,
status: i % 3 === 0 ? 'unread' : 'read',
createdAt: new Date(Date.now() - i * 86400000).toLocaleDateString()
}));
const MessageManager = () => {
const [messages, setMessages] = useState<Message[]>(initialMessages);
const [currentPage, setCurrentPage] = useState(1);
const [modalVisible, setModalVisible] = useState(false);
const [formData, setFormData] = useState({ title: '', content: '' });
const pageSize = 10;
const startIndex = (currentPage - 1) * pageSize;
const paginatedData = messages.slice(startIndex, startIndex + pageSize);
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const handleDelete = (id: string) => {
setMessages(messages.filter(msg => msg.id !== id));
};
const handleMarkRead = (id: string) => {
setMessages(messages.map(msg =>
msg.id === id ? { ...msg, status: 'read' } : msg
));
};
const handleFormSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formData.title || !formData.content) return;
const newMessage: Message = {
id: `msg-${Date.now()}`,
title: formData.title,
content: formData.content,
status: 'unread',
createdAt: new Date().toLocaleDateString()
};
setMessages([newMessage, ...messages]);
setFormData({ title: '', content: '' });
setModalVisible(false);
setCurrentPage(1); // 回到第一页
};
const columns = [
{
key: 'title',
title: '标题',
render: (text: string, record: Message) => (
<strong style={{ color: record.status === 'unread' ? 'red' : 'inherit' }}>
{text}
</strong>
)
},
{ key: 'content', title: '内容' },
{ key: 'status', title: '状态', render: (text: string) => text === 'read' ? '已读' : '未读' },
{ key: 'createdAt', title: '创建时间' },
{
key: 'actions',
title: '操作',
render: (_: any, record: Message) => (
<div>
{record.status === 'unread' && (
<Button size="small" onClick={() => handleMarkRead(record.id)}>
标记已读
</Button>
)}
<Button type="danger" size="small" onClick={() => handleDelete(record.id)} style={{ marginLeft: '8px' }}>
删除
</Button>
</div>
)
}
];
return (
<div style={{ padding: '20px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '16px' }}>
<h2>消息管理</h2>
<Button type="primary" onClick={() => setModalVisible(true)}>
新增消息
</Button>
</div>
<Table
columns={columns}
data={paginatedData}
rowKey={(record) => record.id}
/>
<Pagination
current={currentPage}
total={messages.length}
pageSize={pageSize}
onChange={handlePageChange}
/>
<Modal
visible={modalVisible}
title="新增消息"
onClose={() => setModalVisible(false)}
footer={
<>
<Button onClick={() => setModalVisible(false)}>取消</Button>
<Button type="primary" onClick={handleFormSubmit} style={{ marginLeft: '8px' }}>
提交
</Button>
</>
}
>
<form onSubmit={handleFormSubmit}>
<div style={{ marginBottom: '16px' }}>
<label>标题:</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
style={{ width: '100%', padding: '8px', marginTop: '4px' }}
/>
</div>
<div style={{ marginBottom: '16px' }}>
<label>内容:</label>
<textarea
value={formData.content}
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
rows={4}
style={{ width: '100%', padding: '8px', marginTop: '4px' }}
/>
</div>
</form>
</Modal>
</div>
);
};
export default MessageManager;
七、本章小结
✅ 本章涵盖组件:
组件 | 核心技术点 |
---|---|
Button | props、动态类名、forwardRef、状态管理 |
Modal | Portal、事件冒泡控制、键盘监听 |
Tree | 递归渲染、状态提升、复选逻辑 |
Table | 泛型、插槽渲染、排序、分页联动 |
Pagination | 数学分页算法、边界控制 |
综合案例 | 状态共享、组件通信、数据驱动 UI |
💡 最佳实践建议:
- 所有组件尽量原子化、可复用
- 使用 TypeScript 增强类型安全
- 使用
React.memo
+useCallback
优化性能 - 使用 Context / Zustand / Redux 管理复杂状态
- 样式建议使用 CSS Modules 或 Tailwind
🎯 学习路径建议:
- 先掌握单个组件开发
- 再学习组件间通信(props / callback / context)
- 最后实战综合项目,如后台管理系统、数据看板等
📌 源码结构建议:
src/
├── components/
│ ├── Button/
│ │ ├── index.tsx
│ │ └── Button.css
│ ├── Modal/
│ ├── Tree/
│ └── Table/
│ ├── Table.tsx
│ ├── TableHeader.tsx
│ ├── TableBody.tsx
│ └── Pagination.tsx
├── views/
│ ├── TreeTableLinkage.tsx
│ └── MessageManager.tsx
└── App.tsx
✅ 以上内容可直接用于项目开发,结构清晰、注释完整、功能实用。