很早之前阅读的一篇文章,最近翻到仍觉得有所受益,因此翻译让更多的人看到。
原文地址:
Five common mistakes writing react components (with hooks) in 2020
我在编写 react 组件时发现的最常见的错误是,为什么它们是错误,以及如何避免或修复它们。
React 在 web 开发领域已经有一段时间了。它作为敏捷 web 开发工具的地位近年来稳步加强。特别是在新的 hooks 概念发布之后,编写组件变得前所未有的容易。
尽管背后的团队做出了反应,庞大的社区也试图以一种令人印象深刻的方式培训和解释框架的概念,但我仍然看到在使用框架时出现了一些陷阱和常见错误。我列出了过去几年我所看到的所有与反应有关的错误,特别是使用钩子。在这篇文章中,我想向你展示最常见的几种。我也会试着详细解释,为什么我认为他们是错误的,并建议以一种更干净的方式去做。
在我们开始这个列表之前,我必须说,下面的大多数事情不是根本错误,或者第一眼看上去不是错误的。它们中的大多数都不太可能影响应用程序的性能或外观。可能除了开发产品的开发人员之外,没有人会注意到这里有什么问题,但我仍然相信,高质量的代码可以带来更好的开发体验,从而产生更好的产品。
与任何软件框架或库一样,关于它有数百万种不同的观点。你在这里看到的一切都是基于我的个人观点,不应该被认为是一个普遍的规则。如果你对 ta 有不同的看法,我很乐意听听。
useState
React 的核心概念之一是处理状态。您可以通过状态控制整个数据流和呈现。每次再次呈现树时,它极有可能与状态更改绑定在一起。
使用 useState
钩子,您现在还可以在函数组件中定义状态。这是一种处理 React 状态的简洁而简单的方法。但它也可能被滥用,正如我们在下面的例子中看到的那样。
对于下一个示例,我们需要进行一些解释,假设我们有两个按钮,一个按钮是计数器,另一个按钮发送请求或用当前计数触发一个操作。但是,在组件中永远不会显示当前编号。只有当您单击第二个按钮时,请求才需要它。
function ClickButton(props) {
const [count, setCount] = useState(0);
const onClickCount = () => {
setCount((c) => c + 1);
};
const onClickRequest = () => {
apiCall(count);
};
return (
<div>
<button onClick={onClickCount}>Counter</button>
<button onClick={onClickRequest}>Submit</button>
</div>
);
}
乍一看,你可能会问这到底有什么问题?这不就是建立国家的目的吗?当然你是对的,这将工作只是很好,可能永远不会有问题。然而,在 react 中,每一个状态的改变都会强制该组件和它的子组件进行渲染。但在上面的例子中,因为我们从来没有在渲染部分使用这种状态,所以每次我们设置计数器时,这将成为一个不必要的渲染,这可能会影响性能或产生意想不到的副作用。
如果你想在你的组件中使用一个变量,它应该在渲染之间保持它的值,但又不强制渲染,你可以使用 useRef
钩子。它将保留该值,但不强制组件重新呈现。
function ClickButton(props) {
const count = useRef(0);
const onClickCount = () => {
count.current++;
};
const onClickRequest = () => {
apiCall(count.current);
};
return (
<div>
<button onClick={onClickCount}>Counter</button>
<button onClick={onClickRequest}>Submit</button>
</div>
);
}
router.push
而不是 Link
这可能是一个非常简单和明显的问题,而且与 React 本身并没有太大关系,但我在编写反应组件时仍然经常看到这种情况。
假设您将编写一个按钮,通过单击该按钮,用户将被重定向到另一个页面。因为它是一个SPA,所以这个动作将是一个客户端路由机制。所以你需要一些库来做这个。在react中最流行的是 react-router,下面的例子将使用这个库。
因此,添加一个单击侦听器将用户重定向到所需的页面,对吗?
function ClickButton(props) {
const history = useHistory();
const onClick = () => {
history.push('/next-page');
};
return <button onClick={onClick}>Go to next page</button>;
}
尽管这对于大多数用户来说都是可行的,但是在可访问性方面存在一个巨大的问题。该按钮将不会被标记为链接到其他页面,这使得它几乎不可能被屏幕阅读器识别。你可以在一个新的标签或窗口中打开它吗?很可能不。
在可能的情况下,通过任何用户交互链接到其他页面应该由 组件或普通的
标签处理。
function ClickButton(props) {
return (
<Link to="/next-page">
<span>Go to next page</span>
</Link>
);
}
useEffect
处理 actions
React 推出的最好、最体贴的钩子之一是 useEffect
钩子。它支持处理与 props
或 state
更改相关的操作。尽管它具有有用的功能,但它也经常被用于可能不需要它的地方。
假设有一个组件获取一个项列表并将它们呈现给dom。此外,如果请求成功,我们希望调用“onSuccess”函数,该函数作为道具传递给组件。
function DataList({ onSuccess }) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
const fetchData = () => {
setLoading(true);
callApi()
.then((res) => setData(res))
.catch((err) => setError(err))
.finally(() => setLoading(false));
};
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
if (!loading && !error && data) {
onSuccess();
}
}, [loading, error, data, onSuccess]);
return <div>Data: {data}</div>;
}
有两个 useEffect
钩子,第一个是处理初始渲染的 api
调用,第二个将调用 onSuccess
函数,假设当没有加载,没有错误,但数据在状态,它必须是一个成功的调用。是有意义的对吧?
当然,对于第一次调用,这是真的,可能永远不会失败。但是您也失去了操作和需要调用的函数之间的直接联系。我们不能保证这种情况只会发生在获取操作成功的情况下,这是我们作为开发人员真的不喜欢的事情。
一个直接的解决方案是将 onSuccess
函数设置为调用成功的实际位置:
function DataList({ onSuccess }) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
const fetchData = () => {
setLoading(true);
callApi()
.then((fetchedData) => {
setData(fetchedData);
onSuccess();
})
.catch((err) => setError(err))
.finally(() => setLoading(false));
};
useEffect(() => {
fetchData();
}, []);
return <div>{data}</div>;
}
现在,当 onSuccess
被调用时,第一眼看上去就很清楚了,确切地说,是在 api
调用成功的情况中。
组合组件可能很困难。什么时候可以将一个组件拆分成几个更小的组件?如何构造组件树?在使用基于组件的框架时,所有这些问题每天都会出现。然而,在设计组件时,一个常见的错误是将两个用例合并到一个组件中。让我们以标题为例,它要么显示移动设备上的汉堡按钮,要么显示桌面屏幕上的标签。(这个条件将被神奇的 isMobile 函数处理,它不是这个例子的一部分)
function Header({ menuItems }) {
return (
<header>
<HeaderInner menuItems={menuItems} />
</header>
);
}
function HeaderInner({ menuItems }) {
return isMobile() ? <BurgerButton menuItems={menuItems} /> : <Tabs tabData={menuItems} />;
}
通过这种方法,组件 HeaderInner
试图同时成为两种不同的东西,我们都从 Jekyll 先生那里学到了,一次做不止一件事并不是很理想。这也使得在其他地方测试或重用组件变得更加困难。
将条件提升一级,可以更容易地看到组件的用途,以及它们只有一个职责,即 Header
、Tabs
或 BurgerButton
,而不是试图同时成为两个东西。
function Header(props) {
return (
<header>{isMobile() ? <BurgerButton menuItems={menuItems} /> : <Tabs tabData={menuItems} />}</header>
);
}
useEffects
还记得我们只有 componentWillReceiveProps
或 componentDidUpdate
方法来钩子到 react 组件的渲染过程的时候吗?它找回了黑暗的记忆,也实现了使用 useEffect
钩子的美丽,特别是你可以拥有你想要的任何数量的它们。
但有时遗忘和在一些事情上使用 useEffect
会带回那些黑暗的记忆。例如,假设您有一个组件,它以某种方式从后端获取一些数据,并根据当前位置显示面包屑。(再次使用 react-router 获取当前位置。)
function Example(props) {
const location = useLocation();
const fetchData = () => {
/* Calling the api */
};
const updateBreadcrumbs = () => {
/* Updating the breadcrumbs*/
};
useEffect(() => {
fetchData();
updateBreadcrumbs();
}, [location.pathname]);
return (
<div>
<BreadCrumbs />
</div>
);
}
有两个使用情况,“数据获取”和“显示面包屑”。两者都用同一个 useEffect
钩子更新。这个单一的 useEffect 钩子将在 fetchData
和 updateBreadcrumbs
函数或 location
改变时运行。现在的主要问题是,当 location
发生变化时,我们也会调用 fetchData
函数。这可能是我们没有想到的副作用。
确保分离 effect,他们只用于一个 effect 和意想不到的副作用消失。
function Example(props) {
const location = useLocation();
const updateBreadcrumbs = () => {
/* Updating the breadcrumbs*/
};
useEffect(() => {
updateBreadcrumbs();
}, [location.pathname]);
const fetchData = () => {
/* Calling the api */
};
useEffect(() => {
fetchData();
}, []);
return (
<div>
<BreadCrumbs />
</div>
);
}
额外的好处是,现在使用情况也在组件中按照逻辑排序。
在 react 中编写组件时有很多陷阱。要理解整个机制并避免每一个小的甚至大的错误是不可能的。但在学习框架或编程语言时,犯错误也很重要,可能没有人能 100% 避免这些错误。
我认为与他人分享你的经验会对他人很有帮助,也会防止他们重蹈覆辙。
我仅翻译文章,也是留下学习记录。有任务问题还请指正,谢谢~