5个技巧帮助你避免React Hooks的陷阱

140 阅读11分钟

我已经把这篇博文作为一个演讲,你可以在这里观看。

React Hooks功能是在2018年10月提出的,4个月后于2019年2月发布。从那时起,人们迅速学习并在他们的生产代码库中采用钩子,因为钩子极大地简化了应用程序中的状态和副作用管理。

它绝对占据了它应有的位置,成为 "新的热点"。但是,尽管它很热,React钩子要求你改变对React组件生命周期、状态和副作用的思考方式,如果你没有正确思考React钩子,就很容易陷入有问题的情况。因此,让我们来看看你可能会遇到哪些陷阱,以及如何改变你的思维方式以避免它们。

陷阱1:没有一个好的基础就开始工作

React Hooks文档非常出色,我强烈建议你通读一下,尤其是FAQ,里面有很多有用的信息。所以给自己一两个小时的时间,在不碰键盘的情况下阅读这些文档。这将使你对钩子的概念有一个很好的概述,并为你节省大量的时间。

另外,不要跳过Sophie、Dan和Ryan的React Conf讲座,这些讲座介绍了钩子

为了避免第一个陷阱。阅读文档和FAQ 📚

陷阱2:不使用(或忽略)ESLint插件

在Hooks发布的时候,eslint-plugin-react-hooks 包被建立和发布。它有两个规则。"rules of hooks "和 "exhaustive deps"。这些规则的默认推荐配置是将 "rules of hooks "设置为错误,而将 "exhaustive deps "设置为警告。

我强烈建议你安装、使用并遵守这些规则。它不仅可以捕捉到你很容易错过的真正的bug,而且还可以在这个过程中教给你一些关于你的代码和钩子的东西(更不用说那很棒的自动修复功能)。

我和很多人谈过,他们对详尽的deps插件感到厌烦,所以让我快速演示一个忽略该插件可能导致bug的场景。

想象一下,你有一个显示狗的列表的屏幕,当你点击一只狗时,它会带你到另一个显示该狗信息的页面。 类似这样。

好的,所以对于细节页面,我们做了一个DogInfo 组件,它接受一个dogId 道具并获取狗的信息。

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>
}

我们觉得可以省略数组中的依赖关系,因为这个请求应该只在挂载时提出。以现在的情况来看,我们是可以做到的。但是现在让我们想象一下,用户界面发生了一些变化,我们开始在这个页面上列出一个 "相关狗 "的用户界面。我们会有一个错误,点击相关的狗不会更新狗的信息,即使这个组件被重新渲染了!这就是我们的错误。观察一下(点击 "贵宾犬",然后在 "相关狗 "下点击 "Bernedoodle",注意没有发生变化)。

所以它触发了我们的DogInfo ,用一个新的dogId ,但因为我们提供了一个空的依赖关系数组,我们的效果没有重新运行。

所以让我们添加这个依赖:

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>
}

非常好。下面是它现在的工作情况。

这里是这个例子的关键启示。如果它真的永远不会改变,那么无论如何包括它也没有什么坏处。另外,如果你认为它永远不会改变,而它确实改变了,包括它将帮助你避免bug。

还有很多其他的情况,这些情况更讨厌,也更难识别/解释(比如,如果你跳过将一个函数添加到依赖列表中,你可能会调用一个过时的闭包)。请相信我,每次我想 "哦,这次我不需要遵循这个规则 "的时候,我后来都后悔禁用它,因为我错了。

请注意,由于ESLint等静态分析工具的限制,有时该规则无法对你的代码进行正确的静态分析。我相信这就是为什么建议将详尽的deps规则设置为 "警告 "而不是 "错误"。当这种情况发生时,该插件会在警告中告诉你。我建议你试着重组一下你的代码,以避免该警告(请偏向于明确而非巧妙)。如果这不起作用,那么禁用该插件就是你的逃生舱口,这样你就可以继续工作了。

为了避免第二个陷阱。安装、使用并遵循ESLint插件 👨🏫

陷阱3:用生命周期思考问题

只要React还在流行(在钩子之前),我们就有一个很好的、清晰的组件API,使我们能够很容易地告诉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...
  }
}

像这样编写组件仍然有效(在可预见的未来也是如此),而且多年来效果非常好。钩子有很多好处,但我最喜欢的一点是,它使你的组件更具有声明性,因为它允许你不再考虑 "事情应该在组件的生命周期中何时发生"(这并不重要),而是更多地考虑 "事情应该在与状态变化有关的时候发生"(这更重要)。

所以现在我们有了。

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用另一种方式说得非常好。

@dan_abramov @_developit @mjackson问题不是 "这个效果什么时候运行",问题是 "这个效果与哪个状态同步" useEffect(fn) // 所有状态 useEffect(fn, []) // 无状态 useEffect(fn, [这些, states])

17 330 1,240

我之所以这么喜欢这个方法,是因为它能自然地帮助我避免bug。所以我经常发现我的代码中有一个bug,因为我忘记了在componentDidUpdate ,处理道具或状态的更新,而当我记得的时候,我经常会忘记在启动新的副作用之前清理之前的副作用(例如,如果你做了一个HTTP请求,但是在该请求完成之前,一个道具发生了变化,你应该取消之前的请求)。

有了React Hooks,你仍然要考虑什么时候应该运行副作用,但你不是在考虑组件的生命周期,而是在考虑将副作用的状态与应用程序的状态同步。 掌握这一点需要一点点的学习,但这是一个强大的想法,一旦你理解了它,由于API的设计,你的应用程序中自然会减少错误。

所以当你在想"嘿,我的依赖列表需要[]" 不要因为你认为它只需要在mount上运行而这样做,而是因为你知道它所做的东西永远不会变质。

为了避免这个陷阱。不要考虑生命周期,要考虑将副作用与状态同步🔄。

陷阱4:过度考虑性能

出于某种原因,当一些人看到这一点时,他们会吓坏了。

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

人们担心这个问题有两个原因。

  1. 我们在组件内定义函数,这意味着每次<MyComponent /> ,它都会被重新定义。
  2. 我们将新定义的函数作为道具传递给<SomeOtherComponent /> ,这意味着它不能与React.memoReact.PureComponent 、或shouldComponentUpdate 一起进行适当的优化,并且会遭受 "不必要的重新渲染"。

对于第一点,JavaScript引擎(即使是低端移动设备上的引擎)定义函数的速度非常快。你非常不可能遇到(重新)定义太多的函数的问题。

对于第二点,"不必要的重新渲染 "并不一定对性能有害。仅仅因为一个组件重新渲染,并不意味着DOM会被更新(更新DOM可能很慢)。React在优化自身方面做得很好,所以你不必对你的代码做奇怪的事情来使其快速。它在默认情况下是很快的。

如果你的应用程序的不必要的重新渲染导致你的应用程序很慢,首先要调查为什么渲染会慢。如果你的应用程序的渲染速度很慢,以至于一些额外的重新渲染产生了明显的减速,那么当你点击 "必要的重新渲染 "时,你可能仍然会有性能问题。一旦你解决了导致渲染缓慢的问题,你可能会发现不必要的重新渲染不再给你带来问题了。

如果你确定不必要的重新渲染导致了你的性能问题,那么你可以解开React提供给你的内置性能优化API,如React.memoReact.useMemoReact.useCallback 。从我的博文useMemo和useCallback了解更多。请记住,有时你可以应用性能优化,但你的应用程序实际上运行得更慢!因此,首先要测量所以要先测量!

还要记住,生产版本的react比开发版本的要快

为了避免这个陷阱。了解React的默认速度,在过早地应用性能优化之前做一些调查🏎💨。

陷阱5:过度考虑钩子的测试问题

我注意到有些人担心,当他们重构钩子的时候,他们需要把测试和所有的组件一起重写。这可能是真的,也可能不是真的,取决于你的测试是如何写的。

借用我的文章"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
  render(<Accordion items={items} />)

  expect(screen.getByText(hats.contents)).toBeInTheDocument()
  expect(screen.queryByText(footware.contents)).toBeNull()

  userEvent.click(screen.getByText(footware.title))

  expect(screen.getByText(footware.contents)).toBeInTheDocument()
  expect(screen.queryByText(hats.contents)).toBeNull()
})

这里的关键区别在于,前一个测试的是组件的实现细节,而新的测试不是。无论你的组件是通过Hooks还是作为一个类来实现,都是组件的一个实现细节。因此,如果你的测试是以这样的方式来写的,揭示了这一点(比如使用.state().instance() ),那么将你的组件重构为钩子自然会导致你的测试中断。

但是终端用户并不关心你的组件是用钩子还是用类来写的。他们只关心能否与这些组件呈现在屏幕上的东西进行交互。因此,如果你的测试与正在渲染的东西进行交互,那么这些东西如何被渲染到屏幕上并不重要,无论你使用的是类还是钩子,它都会工作。

你可以从《测试实现细节》和《避免测试用户》中了解更多这方面的信息。

所以,为了避免这个陷阱。避免测试实现细节 🔬

总结。

回顾一下,以下是我想给你的一些建议,帮助你避免钩子的一些陷阱:

  1. 阅读文档和常见问题 📚
  2. 安装、使用并遵循ESLint插件 👨🏫
  3. 不要考虑生命周期,要考虑将副作用与状态同步 🔄
  4. 知道React的默认速度很快,在过早应用性能优化之前做一些调查 🏎💨
  5. 避免测试实现细节 🔬

我希望这对你有帮助!钩子使我的应用程序减少了错误,使我的工作效率提高了。虽然不能否认钩子有一个学习曲线(如果你已经使用了一段时间的React,这个曲线会更尖锐),但这是非常值得投资的。