当我刚开始使用 React 时,一切似乎都很简单 ------ 只有几个组件、一些 props 和状态。但随着项目的增长,我开始遇到一些问题。我意识到这些问题其实是伪装成模式的反模式 ------ 不是好的那种,而是有害的。在这里,我主要指的是我在许多代码库中看到的代码模式,这些代码是由不同经验水平的开发者编写的,并不一定是真正意义上的"模式"。
1. Props 钻透
问题: Props 钻透发生在你将 props 从顶级组件传递到中间组件,最终到达实际需要它们的组件。当你的组件树很深,且 prop 只在链的末端被一个组件使用时,这会显得尤为成问题。
为什么不好: 这会导致组件紧密耦合,难以重构。如果 prop 的需求发生变化,你可能需要更新从顶层到底层之间的每个组件。整个系统会变得脆弱且难以维护。
想象一个 SearchableList
组件,它通过 List
将 onItemClick
函数传递给 ListItem
。每个中间组件都必须处理 props,即使它们实际上并不需要它们。这为了一点点收益增加了大量的复杂性。
代码示例 --- 不好的方法:
jsx
function SearchableList({ items, onItemClick }) {
return (
<div className="searchable-list">
<List items={items} onItemClick={onItemClick} />
</div>
);
}
function List({ items, onItemClick }) {
return (
<ul className="list">
{items.map((item) => (
<ListItem key={item.id} data={item} onItemClick={onItemClick} />
))}
</ul>
);
}
function ListItem({ data, onItemClick }) {
return (
<li className="list-item" onClick={() => onItemClick(data.id)}>
{data.name}
</li>
);
}
你必须将 onItemClick
从 SearchableList
一路传递到 ListItem
,中间的所有组件都包含这个 prop 和函数,但它们什么也不做。
这在人们试图"修复"问题时非常常见,或者他们没有时间去思考影响会有多大。
2. 在组件内进行数据转换
问题: 直接在组件的 useEffect
或渲染函数中转换数据,通常感觉是最简单的做法。你获取数据,按需转换,然后设置状态 ------ 全部在一个地方完成。
为什么不好: 这会在组件内混合关注点,使其承担多个职责 ------ 获取、转换和渲染。这也会使测试变得困难,并限制转换逻辑的可重用性。随着转换变得越来越复杂,这会使组件更难以理解和维护。
考虑一个 UserProfile
组件,它获取用户数据并转换它,例如合并名字和姓氏。将所有这些逻辑放在 useEffect
中意味着每次更改都需要获取和转换 ------ 效率不高。
代码示例 --- 在组件内转换:
jsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(data => {
// 在组件内转换数据
const transformedUser = {
name: `${data.firstName} ${data.lastName}`,
age: data.age,
address: `${data.addressLine1}, ${data.city}, ${data.country}`
};
setUser(transformedUser);
});
}, [userId]);
return (
<div>
{user && (
<>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
<p>Address: {user.address}</p>
</>
)}
</div>
);
}
这个示例展示了如何在 useEffect
中直接数据获取和转换,导致组件紧密耦合,使测试变得困难。
3. 在视图中包含复杂逻辑
问题: 你是否曾经因为"只是一小段"而直接在组件中包含一些业务逻辑?但很快组件就充满了条件语句和计算。
为什么不好: 组件应该专注于呈现 UI,而不是实现业务规则。你在组件中放入的逻辑越多,重用它们就越困难。这会导致组件臃肿,难以测试和理解。
想象一个组件,它不仅显示订单详情,还计算折扣、运费和预估税 ------ 这种逻辑如果放在单独的服务函数或钩子中更具可重用性。
4. 缺乏测试
问题: 跳过测试可能感觉节省时间,尤其是在截止日期时。但 React 组件通常处理复杂功能 ------ 如管理表单状态或 API 调用 ------ 这可能导致难以诊断的错误。
为什么不好: 没有适当的单元或集成测试,当重构或添加功能时,没有安全网来捕获错误。每次更改都成为一项冒险,你会发现自己做了很多手动测试,但仍无法覆盖所有场景。
我还记得在某个功能中,购物车在某些边缘情况下未能更新。适当的单元测试本可以在问题进入生产环境之前捕获这些问题。
5. 重复代码
问题: 复制粘贴一段代码通常是 easiest solution ------ 你已经写过了,为什么不直接重用呢?问题在于,每个重复实例都是维护负担。
为什么不好: 当需求变化时,你需要更新每个重复实例,遗漏其中一个可能会导致错误和不一致。这是确保你的逻辑保持集中且易于修改的问题。
想象一个 formatDate()
函数,它出现在多个组件中,因为你每次需要时都粘贴了它。当格式要求变化时,这会变成一项搜索并希望你找到了所有实例的任务。
代码示例 --- 重复代码:
jsx
function AdminList(props) {
const filteredUsers = props.users.filter(user => user.isAdmin);
return <List items={filteredUsers} />;
}
function ActiveList(props) {
const filteredUsers = props.users.filter(user => user.isActive);
return <List items={filteredUsers} />;
}
这个示例展示了如何在不同组件中过滤用户逻辑,导致代码重复。
6. 责任过多的长组件
问题: 你可能会想到一个像 OrderContainer
这样的组件,它管理与订单相关的所有内容 ------ 验证、错误处理、获取数据和渲染 UI。
为什么不好: 组件应该遵循单一职责原则(SRP)。当它们有太多职责时,它们会变得复杂且难以调试、理解和扩展。如果某部分逻辑依赖于另一部分,它们也会非常难以测试。
在一个项目中,我有一个表单组件,它处理验证、提交、错误显示,甚至管理全局状态。将其拆分为更小的组件并提取不同任务的钩子,使代码更易于处理。