28

编写 React 组件时常见的 5 个错误

 3 years ago
source link: https://www.infoq.cn/article/ZTLzd1LFkB0lMh5mVXyy
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文最初发布于 lorenzweiss.de 网站,经原作者授权由 InfoQ 中文站翻译并分享。

React 框架

React 在 Web 开发领域已经资格不浅了,近年来它作为敏捷 Web 开发工具的角色愈加深入人心。特别是新的 hook API/ 概念发布之后,用 React 编写组件变得非常简单。

尽管 React 背后的团队和庞大的社区在努力推广普及这一框架的相关理念,但很多人在使用它时还是经常会遇到一些陷阱,犯一些常见的错误。我把过去几年中见过的所有 hook 相关的错误用法总结成了一个列表。在本文中,我想向大家展示其中一些最常见的错误,详细解释为什么我认为这些用法不对,并给出较简洁的正确方法的建议。

免责声明

开始以前我必须声明,下面列举的这些事情大都不是根本性的错误,或者初看上去没什么问题,也不大可能影响应用程序的性能或外观。除了产品的开发人员外,也许没人会注意到这里有些问题。但是我仍然相信,高质量的代码可以带来更好的开发体验,进而打造出更好的产品。

与其他任何软件框架或库一样,这里的不同意见数不胜数。本文的所有内容都基于我的个人观点,不应视为一般性规则。如果你有不同的看法,我洗耳恭听。

1. 在不需要重渲染时使用 useState

React 的一个核心概念是处理状态。你可以通过状态控制整个数据流和渲染过程。每次树被重新渲染时,很可能是因为状态的变化。

使用 useState hook,你现在还可以在函数组件中定义状态,这种方法可以真正简洁地在 React 中处理状态。但正如以下示例所示,它也可能被滥用。

关于下面这个示例我们需要说明一下。假设我们有两个按钮,一个按钮是计数器,另一个按钮使用当前计数发送请求或触发动作。但是,当前编号永远不会显示在组件内。当你单击第二个按钮时才需要这个请求。

这很危险:x:

复制代码

functionClickButton(props){
const[count, setCount] = useState(0);
constonClickCount =()=>{
setCount((c) =>c +1);
};
constonClickRequest =()=>{
apiCall(count);
};
return(
<div>
<buttononClick={onClickCount}>Counter</button>
<buttononClick={onClickRequest}>Submit</button>
</div>
);
}

问题:zap:

乍一看,你可能会问这到底有什么问题?状态不就是这样用的吗?你当然没错,它运行很正常,并且可能永远不会出问题,但是在 React 中,每个状态更改都将强制对该组件,很有可能还有其子级进行重渲染,但在上面的示例中,因为我们从未在渲染部分中使用这个状态,结果每次设置计数器时都会有不必要的重渲染,这可能会影响性能或产生意外的副作用。

解决方案:white_check_mark:

如果要在组件内部使用一个变量,希望该变量在渲染之间保持其值,但又不强制重新渲染,则可以使用 useRef hook。它将保留值,但不强制重新渲染组件。

复制代码

functionClickButton(props){
constcount = useRef(0);
constonClickCount =()=>{
count.current++;
};
constonClickRequest =()=>{
apiCall(count.current);
};
return(
<div>
<buttononClick={onClickCount}>Counter</button>
<buttononClick={onClickRequest}>Submit</button>
</div>
);
}

2. 使用 router.push 代替链接

这可能是一个显而易见的错误,其实和 React 本身没什么关系,但是当人们编写 React 组件时经常会犯这种错误。

假设你要编写一个按钮,单击该按钮应将用户重定向到另一个页面。由于它是一个 SPA,因此这个动作是客户端路由机制。于是你需要某种库来执行此动作。在 React 中最流行的是 react-router,下面的示例就会使用它。

所以,添加一个点击侦听器会将用户重定向到所需的页面,对吗?

这很危险:x:

复制代码

functionClickButton(props){
consthistory = useHistory();
constonClick =()=>{
history.push('/next-page');
};
return<buttononClick={onClick}>Go to next page</button>;
}

问题:zap:

就算这段代码对于大多数用户来说都可以正常工作,但这里也有严重的可访问性问题。这个按钮根本不会被标记为链接到另一个页面,于是屏幕阅读器几乎无法识别它。而且你能在新标签页或窗口中打开它吗?很可能做不到。

解决方案:white_check_mark:

只要指向其他页面的链接带有某种用户交互,就要尽量用 < Link> 组件或常规的 < a> 标签处理。

复制代码

functionClickButton(props){
return(
<Linkto="/next-page">
<span>Go to next page</span>
</Link>
);
}

优点:这也使代码更易读,更短!

3. 通过 useEffect 处理动作

React 引入的最好用,最贴心的一个 hook 是 useEffect。它可以处理与 prop 或 state 更改相关的动作。可就算它很好用,人们也不该到处滥用它。

想象一下有一个组件,其获取一个项目列表并将其渲染给 dom。另外,如果请求成功,我们将调用“onSuccess”函数,该函数作为一个 prop 传递给这个组件。

这很危险:x:

复制代码

functionDataList({ onSuccess }){
const[loading, setLoading] = useState(false);
const[error, setError] = useState(null);
const[data, setData] = useState(null);
constfetchData = useCallback(()=>{
setLoading(true);
callApi()
.then((res) =>setData(res))
.catch((err) =>setError(err))
.finally(()=>setLoading(false));
}, []);
useEffect(()=>{
fetchData();
}, [fetchData]);
useEffect(()=>{
if(!loading && !error && data) {
onSuccess();
}
}, [loading, error, data, onSuccess]);
return<div>Data: {data}</div>;
}

问题:zap:

一共有两个 useEffect hooks,第一个在初始渲染时处理 api 调用,第二个会调用 onSuccess 函数,假设当状态没有加载、没有错误但有数据时调用肯定成功。这很有道理是吧?

对第一个调用来说这肯定是正确的,并且可能永远不会失败。但你也失去了动作和需要调用的函数之间的直接联系。同样也没有 100%的保证可以说这种情况仅在 fetch 动作成功后才会发生,而这正是我们开发人员不想看到的。

解决方案:white_check_mark:

一个简单明了的解决方案是将“onSuccess”函数设置为调用成功的实际位置:

复制代码

functionDataList({ onSuccess }){
const[loading, setLoading] = useState(false);
const[error, setError] = useState(null);
const[data, setData] = useState(null);
constfetchData = useCallback(()=>{
setLoading(true);
callApi()
.then((fetchedData) =>{
setData(fetchedData);
onSuccess();
})
.catch((err) =>setError(err))
.finally(()=>setLoading(false));
}, [onSuccess]);
useEffect(()=>{
fetchData();
}, [fetchData]);
return<div>{data}</div>;
}

现在一目了然了,在 api 调用成功的情况下才调用 onSuccess。

4. 单一责任组件

组合组件可能不是什么轻松的事情。什么时候将一个组件拆分为几个较小的组件?如何构造组件树?使用基于组件的框架时,每天都会遇到这些问题。设计组件时常见的一个错误是将两个用例合并到一个组件中。以一个 header 为例,其在移动设备上显示一个汉堡按钮,或在桌面屏幕上显示标签。(这里的条件通过神奇的 isMobile 函数处理,这里就不深入讲解了。)

这很危险:x:

复制代码

functionHeader(props) {
return(
<header>
<HeaderInner menuItems={menuItems} />
</header>
);
}
functionHeaderInner({ menuItems }) {
returnisMobile()? <BurgerButton menuItems={menuItems} /> : <TabstabData={menuItems} />;
}

问题:zap:

使用这种方法时,HeaderInner 组件试图同时兼顾两件事情,而我们都知道一心最好不要二用。而且,这种组件很难在其他地方测试或重用。

解决方案:white_check_mark:

将条件提高一级,这样就能更容易看清组件的本来用途,搞明白它们只应该负责一个任务,不管是 Header、Tab 或 BurgerButton 也好,总之不要一心多用。

复制代码

functionHeader(props) {
return(
<header>{isMobile() ? <BurgerButton menuItems={menuItems} /> : <TabstabData={menuItems} />}</header>
);
}

5. 单一责任的 useEffects

还记得以前,我们只能用 componentWillReceiveProps 或 componentDidUpdate 方法挂接到 React 组件的渲染过程吗?那是一段黑暗的回忆,也让我们意识到了 useEffect hook 的美妙之处,尤其是你可以随意使用这些 hooks。

但是有时因为粗心而让“useEffect”身兼数职,就会带回那些黑暗的回忆。例如,假设你有一个组件以某种方式从后端获取一些数据,并且还会根据当前位置显示面包屑。(再次使用 react-router 获取当前位置。)

这很危险:x:

复制代码

functionExample(props){
constlocation = useLocation();
constfetchData = useCallback(()=>{
/* Calling the api */
}, []);
constupdateBreadcrumbs = useCallback(()=>{
/* Updating the breadcrumbs*/
}, []);
useEffect(()=>{
fetchData();
updateBreadcrumbs();
}, [location.pathname, fetchData, updateBreadcrumbs]);
return(
<div>
<BreadCrumbs/>
</div>
);
}

问题:zap:

这里有两个用例,即“数据获取”和“显示面包屑”。两者都通过 useEffect hook 更新。当 fetchData 和 updateBreadcrumbs 函数或 location 更改时,都会运行这个 useEffect hook。现在的主要问题是,当位置更改时,我们还调用了 fetchData 函数。这可能是我们没有想到的副作用。

解决方案:white_check_mark:

把效果拆分开来,确保它们只用于一种效果,意外的副作用也就消失了。

复制代码

functionExample(props){
constlocation = useLocation();
constupdateBreadcrumbs = useCallback(()=>{
/* Updating the breadcrumbs*/
}, []);
useEffect(()=>{
updateBreadcrumbs();
}, [location.pathname, updateBreadcrumbs]);
constfetchData = useCallback(()=>{
/* Calling the api */
}, []);
useEffect(()=>{
fetchData();
}, [fetchData]);
return(
<div>
<BreadCrumbs/>
</div>
);
}

额外的收获是,这些用例现在也在组件内按顺序排好了。

小结

在 React 中编写组件时有很多陷阱。我们不可能百分百地了解整个机制并避开所有小错,就算是大错误也可能逃不开。但是在学习框架或编程语言时犯错误也是很重要的,可能没有人会 100%摆脱这些错误。

我认为与他人分享你的经验是很有意义的,这样别人就可以避开这些坑了。

如果你有任何疑问,请写信给我([email protected]),我很想听听你的意见。

原文链接:

https://www.lorenzweiss.de/common_mistakes_react_hooks/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK