通常,当一个新的软件开发团队成立时,团队成员会对如何编写代码进行一些讨论。当编程语言或主流框架的维护者对代码的编写有自己想法时,这有助于给团队的这些讨论提供一个默认的起点。
众所周知,React 在某些方面并不具有倾向性,但最新的 React 文档实际上就如何编写 React 代码提出了不少建议。由于这些文档最初是以测试版的形式发布的,我和其他成员越来越多地将它们作为我们团队代码风格讨论的基础。
在这篇文章中,我将推荐 React 文档中的一些建议,这些建议在我团队中讨论频率最高。对于每个建议,我将分享一个简短的摘要和一个代码片段。通过点击 React 文档相关部分的链接,可以找到更多信息和基本原理。
其中一些建议可能会让人感觉像是代码风格的意见,没有任何实际影响。但是,正如 React 文档所解释的那样,在每种情况下,偏离建议都会带来成本或风险。你可以自由地做出自己的决定,但请记住,React 核心团队对这些问题的考虑可能比你或我要多得多。这并不意味着他们总是正确的,但至少听取他们的意见是个好主意。
- [1. 在为循环中的元素选择 key 时,应使用对同一条目始终保持相同的标识符,而不是使用数组索引](#1. 在为循环中的元素选择 key 时,应使用对同一条目始终保持相同的标识符,而不是使用数组索引 "#item1")
- [2. 定义组件时,应将其定义在文件/模块的顶层,而不是嵌套在其他组件或函数中](#2. 定义组件时,应将其定义在文件/模块的顶层,而不是嵌套在其他组件或函数中 "#item2")
- [3. 决定在状态中存储什么内容时,记住可通过已有状态计算得到的内容无需存储在状态中](#3. 决定在状态中存储什么内容时,记住可通过已有状态计算得到的内容无需存储在状态中 "#item3")
- [4. 在考虑是否使用 useMemo、useCallback 或 React.memo 进行缓存时,请推迟使用缓存,直到发现性能问题](#4. 在考虑是否使用 useMemo、useCallback 或 React.memo 进行缓存时,请推迟使用缓存,直到发现性能问题 "#item4")
- [5. 将共享代码提取到函数中时,只有在调用到其他 hook 时才将其命名为 hook](#5. 将共享代码提取到函数中时,只有在调用到其他 hook 时才将其命名为 hook "#item5")
- [6. 当你需要根据 prop 变化调整 state 时,请直接在组件函数中(在渲染过程中)而不是在 effect 中设置状态](#6. 当你需要根据 prop 变化调整 state 时,请直接在组件函数中(在渲染过程中)而不是在 effect 中设置状态 "#item6")
- [7. 需要获取数据时,优先使用库,而不是 useEffect](#7. 需要获取数据时,优先使用库,而不是 useEffect "#item7")
- [8. 当需要对用户操作做出响应时,应在 event handler 中编写代码,而不是在 useEffect 中编写代码](#8. 当需要对用户操作做出响应时,应在 event handler 中编写代码,而不是在 useEffect 中编写代码 "#item8")
- [9. 当一个 useEffect 的依赖导致了你不想要的重复渲染(包括无限循环)时,不要只从数组中移除依赖,也要从 effect 函数中移除依赖](#9. 当一个 useEffect 的依赖导致了你不想要的重复渲染(包括无限循环)时,不要只从数组中移除依赖,也要从 effect 函数中移除依赖 "#item9")
1. 在为循环中的元素选择 key 时,应使用对同一条目始终保持相同的标识符,而不是使用数组索引
React 使用键值在不同渲染中跟踪列表元素。如果元素被添加、删除或重新排序,索引键就会误导 React,从而导致错误。
jsx
// 🛑 WRONG
return (
<ul>
{items.map((item, index) => (
<li key={index}>...</li>
))}
</ul>
);
// 🟢 RIGHT, assuming item.id is a stable unique identifier
return (
<ul>
{items.map((item, index) => (
<li key={item.id}>...</li>
))}
</ul>
);
2. 定义组件时,应将其定义在文件/模块的顶层,而不是嵌套在其他组件或函数中
有时,在另一个组件中定义一个组件似乎很方便。但这将导致组件在每次渲染时都被视为不同的组件,从而导致性能低下。
jsx
// 🛑 WRONG
function ParentComponent() {
// ...
function ChildComponent() {...}
return <div><ChildComponent /></div>;
}
// 🟢 RIGHT
function ChildComponent() {...}
function ParentComponent() {
return <div><ChildComponent /></div>;
}
3. 决定在状态中存储什么内容时,记住可通过已有状态计算得到的内容无需存储在状态中
这使得状态更新变得容易,同时不会引入错误,因为它避免了不同的状态项因过时而变得不一致。
jsx
// 🛑 WRONG
const [allItems, setAllItems] = useState([]);
const [urgentItems, setUrgentItems] = useState([]);
function handleSomeEvent(newItems) {
setAllItems(newItems);
setUrgentItems(newItems.filter(item => item.priority === 'urgent'));
}
// 🟢 RIGHT
const [allItems, setAllItems] = useState([]);
const urgentItems = allItems.filter(item => item.priority === 'urgent');
function handleSomeEvent(newItems) {
setAllItems(newItems);
}
4. 在考虑是否使用 useMemo、useCallback 或 React.memo 进行缓存时,请推迟使用缓存,直到发现性能问题。
虽然总是缓存没有什么大的坏处,但小的坏处是会降低代码的可读性。
React 文档中相关内容:useMemo、useCallback、React.memo
jsx
// 🛑 WRONG
const [allItems, setAllItems] = useState([]);
const urgentItems = useMemo(() => (
allItems.filter(item => item.status === 'urgent'
), [allItems]);
// 🟢 RIGHT (until an observed performance problem)
const [allItems, setAllItems] = useState([]);
const urgentItems = allItems.filter(item => item.priority === 'urgent');
5. 将共享代码提取到函数中时,只有在调用到其他 hook 时才将其命名为 hook
如果你的函数调用了其他 hook,那么它本身就必须是一个 hook,这样 React 加上限制保证 hook 正常运行。如果您的函数不调用其他 hook,那么就没有理由选择让函数受到限制。作为一个非 hook,你的函数将更具通用性,因为它可以从任何地方调用,包括在条件中。
jsx
// 🛑 WRONG
function useDateColumnConfig() { // will be subject to hooks restrictions
return {
dataType: 'date',
formatter: prettyFormatDate,
editorComponent: DateEditor,
};
}
// 🟢 RIGHT
function getDateColumnConfig() { // can be called anywhere
return {
dataType: 'date',
formatter: prettyFormatDate,
editorComponent: DateEditor,
};
}
function useNameColumnConfig() { // has to be a hook since it calls a hook: useTranslation
const { t } = useTranslation();
return {
dataType: 'string',
title: t('columns.name'),
};
}
6. 当你需要根据 prop 变化调整 state 时,请直接在组件函数中(在渲染过程中)而不是在 effect 中设置状态
如果计划根据 prop 变化调整 state,最好先确认是否真的需要。如果能在渲染过程中根据 prop 计算出数据(见上文[建议 3](#建议 3 "#item3"))或使用 key 重置所有状态,那就更好了。
如果你确实需要调整部分 state, 考虑 React 文档关于 effects 的一个关键点是有帮助的。 effects "是 React 范式的逃生舱门。它们让您'跳出' React,将您的组件与某些外部系统同步......" 当你只需要根据 prop 变化进行 state 快速更新时,就不需要这种复杂性了。
感谢 React 文档提供的示例代码(略有简化)!
jsx
// 🛑 WRONG
function List({ items }) {
const [selection, setSelection] = useState(null);
useEffect(() => {
setSelection(null);
}, [items]);
//...
}
// 🟢 RIGHT
function List({ items }) {
const [prevItems, setPrevItems] = useState(items);
const [selection, setSelection] = useState(null);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
//...
}
7. 需要获取数据时,优先使用库,而不是 useEffect
使用 useEffect 获取数据时可能会出现一些微妙的错误,除非你编写大量的模板代码来处理这些错误。React 文档提供了推荐了许多优秀的数据获取库。
jsx
// 🛑 WRONG
const [items, setItems] = useState();
useEffect(() => {
api.loadItems().then(newItems => setItems(newItems));
}, []);
// 🟢 RIGHT (one library option)
import {useQuery} from '@tanstack/react-query';
const { data: items } = useQuery(['items'], () => api.loadItems());
8. 当需要对用户操作做出响应时,应在 event handler 中编写代码,而不是在 useEffect 中编写代码
这样可以确保每次事件发生时代码只运行一次。
React 文档中 effects vs. events,或者可以在油管上搜索 effects vs. events。
jsx
const [savedData, setSavedData] = useState(null);
const [validationErrors, setValidationErrors] = useState(null);
// 🛑 WRONG
useEffect(() => {
if (savedData) {
setValidationErrors(null);
}
}, [savedData]);
function saveData() {
const response = await api.save(data);
setSavedData(response.data);
}
// 🟢 RIGHT
async function saveData() {
const response = await api.save(data);
setSavedData(response.data);
setValidationErrors(null);
}
9. 当一个 useEffect 的依赖导致了你不想要的重复渲染(包括无限循环)时,不要只从数组中移除依赖,也要从 effect 函数中移除依赖
要理解为什么值得这样做可能会有点难;为此,我建议阅读 React 文档中专门介绍 useEffect 的页面。简而言之,使用你未在依赖数组中列出的依赖可能意味着 effect 被用于其他用途,而非 effect 的本意:同步。这迟早会导致难以诊断的 bug。
付诸实践
我希望 React 文档中的这些要点能帮助你学习一种新技术、更深入地理解一种技术或向他人解释一种技术。你在 React 项目的开发中有遵循这些建议吗?React 文档中是否还有其他建议经常出现在你的项目中?