原文链接:The nine best recommendations in the new React docs
通常,当一个新的软件开发团队形成的时候,这个新团队总会讨论该怎么编写他们的代码。当编码语言或框架的维护者对编码提出了自己的观点,这些观点就可以给这个软件开发团队的讨论提供一个默认的起点。
React 在某种方面,以不发表自己关于编码的观点而出名,但是最新的React文档确实给了几个有关你如何编写React代码的建议。自从这些文档在测试版发布,我和其他的使用者就在关于编码风格的谈话中越来越多的引用其中的建议,甚至把这些建议作为了我们在团队中展开讨论的基础。
在这篇文章中,我将重点介绍在我的团队讨论React代码风格时最常出现的来自React文档的建议。对每个文档中提及的建议,我都会给一个简短的总结并附带一个代码片段。更多的信息和基本原理可以在下面的 React 的链接中找到React 文档中的相关部分。
一些建议可能感觉更多的是代码风格的建议,并不会带来真正的结果。但是,正如 React 文档解释的,在每个偏离了这些建议的编码例子都带有成本或风险。你可以自由的去做选择,但是记住React的核心团队比你思考的更完善。这并不意味着他们总是对的,但是听听他们的建议至少没错。
1. 在为循环中的元素选择键时,应使用对同一入口始终相同的标识符,而不是数组索引
在渲染过程中,React用key去追踪列表元素。如果一个元素被新增,删除,或重排,那么错误的索引键会误导React,并导致bug。
js
// 🛑 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. 当定义一个组件时,在文件/模块的顶层定义它,而不是嵌套在另一个组件或函数中
有时,在组件内部定义另一个组件看起来很方便。但是这会导致组件在每次渲染的时候被认为表现有不同,导致低的性能表现。
js
// 🛑 WRONG
function ParentComponent() {
// ...
function ChildComponent() {...}
return <div><ChildComponent /></div>;
}
// 🟢 RIGHT
function ChildComponent() {...}
function ParentComponent() {
return <div><ChildComponent /></div>;
}
3. 当考虑将什么数据存在state里的时候,存储可以用来拼凑所有你需要的数据的最小的数据单元。
这简化了状态,并在更新状态时不容易引入bug,因为这种方式排除了不同的状态在更新时变得不一致。
js
// 🛑 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的时候,如果没有一个直观的性能问题就别用
尽管总使用缓存化没什么太大的坏处,但是有个小问题是它会使我们的代码不那么可读。
js
// 🛑 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. 在将共享代码提取到函数中时,只有在调用其他钩子时才将其命名为钩子
如果你封装的函数调用了其他的hooks,他就需要被封装为一个hooks,这样React就可以对允许它们工作的钩子施加限制。如果你封装的函数没有调用其他的hooks,那就没有理由加入这些限制。你的非hook函数可以有更多的功能,因为它可以在任何地方,任何情况调用。
js
// 🛑 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. 当props改变,你需要随之改变state中的值的时候,直接在组件里改变状态,而非在副作用函数里
如果你打算调整state以响应prop的改变,你需要确定你是否真的需要去改变state。更普遍的做法是在渲染过程中派生数据或者用一个key去重置所有数据。
如果你确实需要去调整state中的部分数据,考虑一下React文档中关于副作用的一个关键点是有帮助的:它们"是React范式的逃生舱口"。它们让你'走出'React,让你的组件与一些外部系统同步......"
所以当你仅仅需要一个简单的state改变去响应prop改变的时候,这个复杂度是不太够去使用副作用函数的。
js
// 🛑 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 文档就好的数据获取库提供了大量的建议。
js
// 🛑 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. 当你需要采取行动去处理发生的事件时候,在事件处理程序里写代码,而非在副作用函数里面
这样写代码能够确定每次事件发生的时候函数只执行一次。
js
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依赖导致你并不需要的重复渲染的时候,不仅仅要把数据从array中移除,还需要从副作用函数中移除
为什么我们做出的改变值得可能很难去理解。为了让你理解,我建议你读下React文档里有关useEffect的部分。简而言之,使用你并没有放在第二个参数(array)列表里的依赖可能意味着这个副作用被用于其他用途,而不是其预期用途:同步。这可能迟早会导致难以诊断的bug。