[译]5个技巧:避免React Hooks 常见问题

[译]5个技巧:避免React Hooks 常见问题

原文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

问题1:在理解以前就急于使用Hooks

React Hooks官方文档 写得很是详尽,我强烈建议你在使用 Hooks 以前,把官方文档 通读 一遍,尤为是 FAQ 部分,里面包含了不少实际开发中会遇到的问题及解决办法。给你本身一两个小时,通读一下官方文档吧,这将对你理解 Hooks 有很大的帮助,而且在未来的实际开发中帮你节省不少(找bug和改bug的)时间。git

与此同时,建议你也看一看 Sophie, Dan 和 Ryan介绍Hooks的分享github

第一个问题的解决办法:仔细研读官方文档以及FAQ 📚数组

问题2:不使用(或忽视)React Hooks的ESLint插件

在 React Hooks发布的同时,eslint-plugin-react-hooks 这个 ESLint的插件也发布了。这个插件包含两个校验规则:rules of hooksexhaustive 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 ,可是因为咱们在 DogInfouseEffect 依赖项中,写的空数组,致使这个 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 ** 。

问题3:(错误地)从组件生命周期角度来思考Hooks

在 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 里处理了 propstate 的变化,可是却忘记了取消掉上一次变化所引发的反作用。举个栗子,你发起了某次 HTTP 请求,可是在 HTTP 完成以前,组件的某个 prop 或 state 发生了变化,那么你一般应该取消掉这个HTTP请求。

在使用 React Hooks 的场景下,你仍然须要思考你的反作用在什么时机执行,可是你不用再纠结反作用是在哪一个生命周期里执行,你思考的是,怎么保持反作用的结果和组件的状态同步。要理解这个点,须要付出一些努力,可是你一旦理解到了,你将避免不少的bug。

所以,你能够给 useEffect 的依赖项设置为 空数组 的唯一理由,是它确实没有依赖任何外部变量,而 不是 你认为这个反作用只须要在组件mount的时候执行一次。

第三个问题解决办法:不要以组件生命周期的方式来思考 React Hooks,应该是思考,若是让你的反作用和组件状态保持一致

问题4:过于担忧性能

一些开发者看到下面的代码时,他们吓坏了:

function MyComponent() {
  function handleClick() {
    console.log('clicked some other component')
  }
  return <SomeOtherComponent onClick={handleClick} /> } 复制代码

他们一般由于下面2个缘由而感到担心:

  1. 咱们在 MyComponent 内部定义了函数handleClick,这意味着,每次 MyComponent render时,都会从新定义一个不一样的handleClick
  2. 每次render,咱们都将一个新的handleClick传给了 SomeOtherComponent,这意味着咱们不能经过 React.memoReact.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.memoReact.useMemo以及 React.useCallback。你能够从个人这篇博客,了解到 useMemo和useCallback 。注意:有的时候,你采起了性能优化措施以后,你的APP反而更卡顿了……所以,务必在性能优化的先后,作好性能检测对比。

同时记住,production版本的React性能比development版本高不少

第四个问题解决办法:记住一点,React原本就执行很快,不要过早的担忧或者优化你的性能

问题5:对Hooks的测试过于重视

我注意到有些开发者担忧,若是他们讲组件迁移到 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 问题的解决办法:

  1. 仔细研读官方 Hooks 文档,以及 FAQ 部分
  2. 安装、使用而且遵照 eslint-plugin-react-hooks 这个 ESLint 插件
  3. 忘掉组件生命周期的思考方式吧。正确的姿式:怎么保持反作用和组件状态的同步。
  4. React 自己执行很快,在过早的性能优化以前,必定作好相关的知识点调研
  5. 避免测试组件的实现细节,应该关注组件的输入和输出
相关文章
相关标签/搜索