原文: kentcdodds.com/blog/react-…javascript
在这篇文章里,咱们来探索下 React Hooks 的常见问题,以及怎么来避免这些问题。html
React Hooks 是在 2018年10月提出 ,而且在2019年2月 发布 。自从 React Hooks 发布之后,不少开发者都在项目中使用了Hooks,由于Hooks确实在很大程度上简化了咱们对组件 state
和 反作用
的管理。java
毫无疑问,React Hooks 目前是react生态中一个热点,愈来愈多的开发者以及开源库,都引入了Hooks。尽管React Hooks 如今收到追捧,可是它的引入,也须要开发者改变本身对于组件生命周期、state以及反作用的思考方式;若是你没有很好的理解 React Hooks,盲目的使用它将会给你带来一些意想不到的bug。OK,接下来咱们就来看看使用Hooks可能有哪些坑,以及怎么改变咱们的思考方式来避免这些坑。react
React Hooks官方文档 写得很是详尽,我强烈建议你在使用 Hooks 以前,把官方文档 通读 一遍,尤为是 FAQ 部分,里面包含了不少实际开发中会遇到的问题及解决办法。给你本身一两个小时,通读一下官方文档吧,这将对你理解 Hooks 有很大的帮助,而且在未来的实际开发中帮你节省不少(找bug和改bug的)时间。git
与此同时,建议你也看一看 Sophie, Dan 和 Ryan介绍Hooks的分享 。github
第一个问题的解决办法:仔细研读官方文档以及FAQ 📚数组
在 React Hooks发布的同时,eslint-plugin-react-hooks 这个 ESLint
的插件也发布了。这个插件包含两个校验规则:rules of hooks
和 exhaustive deps
。默认的推荐配置,是将 rules of hooks
设置为 error
级别,将 exhaustive deps
设置为 warning
级别。性能优化
我强烈建议你安装、使用并遵照这两条规则。它不只仅能帮你发现容易被忽略的bug,在这个过程当中,还能教你一些代码和hooks的知识,固然了,还有它提供的代码自动修复功能,超cool。app
在我和不少开发者交流中,发现不少人开发者对 exhaustive deps
这条规则感到困惑。所以,我写了一个简单的demo,来展现若是忽略了这条规则,将会致使什么bug。ide
假设咱们有2个页面:一个是 狗狗🐶 列表页 List
,展现一系列狗狗的名字;一个是某一只狗狗的详情页Detail
。在列表页上,点击某个狗狗的名字,就会打开对应狗狗的详情页。
OK,在狗狗详情页上,咱们有一个展现狗狗详情的组件 DogInfo
,它接收狗狗的 dogId
,而且根据 dogId
请求API获取对应的详情:
function DogInfo({dogId}) {
const [dog, setDog] = useState(null)
// imagine you also have loading/error states. omitting to save space...
useEffect(() => {
getDog(dogId).then(d => setDog(d))
}, []) // 😱
return <div>{/* render the dog info here */}</div>
}
复制代码
上面的代码里,咱们 useEffect
的依赖列表是一个空数组,由于咱们只但愿在组件 mount
的时候才去发起一次请求。到目前为止,这段代码没什么问题。如今,假设咱们的狗狗详情页UI有了一点改动,增长了一个 “相关狗狗”的列表。这时咱们的代码就有bug了,点击 “相关狗狗” 列表中的某一只,咱们的 DogInfo
组件并不会更新到对应的狗狗详情,尽管 DogInfo
组件已经从新 render
了。
如今的状况是,点击 “相关狗狗” 列表中的某一项,触发了详情页的从新 render
,而且会把点击的狗狗 dogId
传给 DogInfo
,可是因为咱们在 DogInfo
的 useEffect
依赖项中,写的空数组,致使这个 useEffect
不会从新执行。
嗯,下面是修改以后的代码:
function DogInfo({dogId}) {
const [dog, setDog] = useState(null)
// imagine you also have loading/error states. omitting to save space...
useEffect(() => {
getDog(dogId).then(d => setDog(d))
}, [dogId]) // ✅
return <div>{/* render the dog info here */}</div>
}
复制代码
经过这个栗子,咱们能够得出这个关键结论:若是 useEffect
的某个依赖项真的永远不会改变,那把这个依赖项添加到 uesEffect
的依赖数组里,也没有任何问题。同时,若是你认为某个依赖项不会改变,但实际上这个依赖项却变了,这正好帮助你发现了代码里的bug。
和这个例子相比,还有不少其余的场景更难辨别和分析,好比,你在 useEffect
里调用了某个函数(函数定义在 useEffect 外面),可是却 没有 在依赖项里添加这个函数,那么代码极可能有bug了。相信我,每次在我忽略了这条规则以后,我都会后悔当初为何没有遵照规则。
请注意,受限于 ESLint
在代码静态分析的一些限制,这条规则 (exhaustive deps
)有时候不能正确的分析出你代码中的问题。可能这就是它为何默认设置级别是 warning
而不是 error
的缘由吧。当它不会正确的分析你的代码时,它会给出你一些warning信息,在这种状况下,我建议你稍微重构下你的代码,来保证能正确的被分析。若是重构代码以后,依然不能被正确的分析,那么可能局部的关闭这条规则,也是一个办法吧,为了能继续coding而不至于延期。
第二个问题的解决办法:**安装、使用而且遵照 ESLint ** 。
在 React Hooks 出现以前,咱们能够在类组件里,经过内置的组件生命周期方法,来告诉react,何时,它应该作什么操做:
class LifecycleComponent extends React.Component {
constructor() {
// initialize component instance
}
componentDidMount() {
// run this code when the component is first added to the page
}
componentDidUpdate(prevProps, prevState) {
// run this code when the component is updated on the page
}
componentWillUnmount() {
// run this code when the component is removed from the page
}
render() {
// call me anytime you need some react elements...
}
}
复制代码
在 Hooks 发布以后,像上面这些写类组件一样没问题(在可预见的未来,这样写也没有任何问题),这种类组件的方式已经存在了许多年。Hooks 带来了一系列的好处,其中我最喜欢的一个好处是(useEffect
),Hooks 使得组件更加的符合声明式 语法。有了 Hooks,咱们能够不用去分辨“某个操做应该在组件的哪个生命周期执行”,而是更加直观的告诉 React,“当哪些变化发生时,我但愿执行对应的操做”。
所以,如今咱们的代码长这样:
function HookComponent() {
React.useEffect(() => {
// This side effect code is here to synchronize the state of the world
// with the state of this component.
return function cleanup() {
// And I need to cleanup the previous side-effect before running a new one
}
// So I need this side-effect and it's cleanup to be re-run...
}, [when, any, ofThese, change])
React.useEffect(() => {
// this side effect will re-run on every single time this component is
// re-rendered to make sure that what it does is never stale.
})
React.useEffect(() => {
// this side effect can never get stale because
// it legitimately has no dependencies
}, [])
return /* some beautiful react elements */
}
复制代码
Ryan Florence 从另外一个角度来解释思考方式上的变化 。
我喜欢这个特性(useEffect)的一个主要缘由是,它帮助我避免了不少bug。在过去基于类组件的开发过程当中,我发现引入bug的不少情形,都是我忘记了在 componentDidUpdate
里处理某个 prop
或者 state
的变化;另外一种状况是,我在 componentDidUpdate
里处理了 prop
或 state
的变化,可是却忘记了取消掉上一次变化所引发的反作用。举个栗子,你发起了某次 HTTP 请求,可是在 HTTP 完成以前,组件的某个 prop 或 state 发生了变化,那么你一般应该取消掉这个HTTP请求。
在使用 React Hooks 的场景下,你仍然须要思考你的反作用在什么时机执行,可是你不用再纠结反作用是在哪一个生命周期里执行,你思考的是,怎么保持反作用的结果和组件的状态同步。要理解这个点,须要付出一些努力,可是你一旦理解到了,你将避免不少的bug。
所以,你能够给 useEffect
的依赖项设置为 空数组 的唯一理由,是它确实没有依赖任何外部变量,而 不是 你认为这个反作用只须要在组件mount的时候执行一次。
第三个问题解决办法:不要以组件生命周期的方式来思考 React Hooks,应该是思考,若是让你的反作用和组件状态保持一致
一些开发者看到下面的代码时,他们吓坏了:
function MyComponent() {
function handleClick() {
console.log('clicked some other component')
}
return <SomeOtherComponent onClick={handleClick} /> } 复制代码
他们一般由于下面2个缘由而感到担心:
MyComponent
内部定义了函数handleClick
,这意味着,每次 MyComponent
render时,都会从新定义一个不一样的handleClick
handleClick
传给了 SomeOtherComponent
,这意味着咱们不能经过 React.memo
,React.PureComponent
或者 shouldComponentUpdate
来优化 SomeOtherComponent
的性能,这会引发SomeOtherComponent
许多没必要要的从新render针对第一个问题,JavaScript引擎(即便是在很低端的手机上)定义一个新函数的执行是很是快的。你基本上不会遇到因为重复定义函数而致使你的APP性能低下。
第二个问题来说,没必要要的重复render,也不是必定会引发性能问题。仅仅由于组件从新render了,并不表明实际的DOM会被修改,一般修改DOM才是慢的地方。React 在性能优化方面作的很是好,一般你没有必要为了提高性能去引入一些额外的工做。
若是这些额外的重复render致使你的APP慢,首先应该明确为何重复render会这么慢。若是一次render自己都很慢,致使额外的重复render引发APP卡顿,那么即便你避免了额外的重复render,你极可能仍然面临性能问题。当你修复了致使render慢的缘由以后,你或许会发现,那些重复的render也不会引发APP卡顿了。
若是你真的确认,是额外的重复render致使了APP性能问题,那么你可使用 React 内置的一些性能优化 API,好比 React.memo
,React.useMemo
以及 React.useCallback
。你能够从个人这篇博客,了解到 useMemo和useCallback 。注意:有的时候,你采起了性能优化措施以后,你的APP反而更卡顿了……所以,务必在性能优化的先后,作好性能检测对比。
同时记住,production版本的React性能比development版本高不少 。
第四个问题解决办法:记住一点,React原本就执行很快,不要过早的担忧或者优化你的性能 。
我注意到有些开发者担忧,若是他们讲组件迁移到 React Hooks,他们须要重写对应的全部测试代码。根据你的测试代码实现方式,这个担心可能有道理,也可能没有道理。
引用我本身文章 使用React Hooks,测试代码怎么办? ,若是你的测试代码长这样:
test('setOpenIndex sets the open index state properly', () => {
// using enzyme
const wrapper = mount(<Accordion items={[]} />) expect(wrapper.state('openIndex')).toBe(0) wrapper.instance().setOpenIndex(1) expect(wrapper.state('openIndex')).toBe(1) }) 复制代码
若是是这种状况,那么你正好借着重写测试代码的机会,去优化这些测试代码。毫无疑问,你应该废弃掉上面这样的代码,改为下面这样的:
test('can open accordion items to see the contents', () => {
const hats = {title: 'Favorite Hats', contents: 'Fedoras are classy'}
const footware = {
title: 'Favorite Footware',
contents: 'Flipflops are the best',
}
const items = [hats, footware]
// using React Testing Library
const {getByText, queryByText} = render(<Accordion items={items} />) expect(getByText(hats.contents)).toBeInTheDocument() expect(queryByText(footware.contents)).toBeNull() fireEvent.click(getByText(footware.title)) expect(getByText(footware.contents)).toBeInTheDocument() expect(queryByText(hats.contents)).toBeNull() }) 复制代码
这两份测试代码的关键区别是,旧的代码是在测试 组件的具体实现,新的测试却不是这样。无论组件是基于类的实现方式,仍是基于 Hooks 的方式,都是组件内部的具体实现细节。所以,若是你的测试代码,会放到到被测试组件的一些具体实现细节,(好比 .state()
或者 .instance()
),那么将组件重构为 Hooks 版本,确实会让你的测试代码失效。
可是使用你组件的开发者,是不关心你的组件是基于类实现,仍是基于 Hooks 实现。他们只关心你的组件可以正确的实现业务逻辑,或者说只关心你的组件渲染到屏幕上的内容。所以,若是你的测试代码,是检查组件渲染到屏幕上的内容,那么无论你的组件是基于类仍是Hooks实现,都不影响测试代码的运行。
你能够从这两篇文章了解更多关于测试方面的内容:对实现细节的测试 和 Avoid the Test User 。
OK,解决这个问题的办法是:避免去测试组件的实现细节 。
说了这么多,总结起来就是下面这些建议,帮你避免常见的 Hooks 问题的解决办法: