为什么X不是Hook
Why Isn’t X a Hook?
自从React Hooks的第一个alpha版本发布以来,有一个问题不断出现在讨论中:“为什么<其他API>不是一个钩子?”
提醒你,这里有一些Hooks:
useState()允许你声明一个状态变量
useEffect()允许你声明一个副作用
useContext()
允许你读取一些上下文
还有其他一些API,比如React.memo()
和<Context.Provider>
,但它们不是Hooks。通常提出的Hook版本是非复合或反模块的。本文将帮助你了解原因。
注意:对于那些对API讨论感兴趣的人来说,这篇文章非常深入。你不需要考虑这些就可以提高React的效率!
我们希望React API保留两个重要属性:
调试: 我们希望随着应用程序的增长,很容易找到错误。React的最佳功能之一是,如果你看到错误的渲染,你可以到树上,直到找到是哪个组件的prop或状态导致的错误。
这两个约束放在一起可以告诉我们什么可以或不可以是一个Hook。我们来试试几个例子吧。
真正的Hook: useState()
useState()
组合
每个调用useState()
的多个自定义Hook都不会产生冲突:
添加新的无条件的useState()
调用始终是安全的。你不需要了解组件用于声明新状态变量的其他Hook。你也不能通过更新其中一个来破坏其他状态变量。
结论✅ : useState()
不会使自定义Hook轻易出问题。
调试
Hooks很有用,因为你可以在Hooks 之间 传递值:
但是,如果我们犯了错误怎么办?什么是调试故事?
假设我们从theme.comment
获得的CSS
类名是错误的。我们如何调试这个?我们可以在组件的主体中设置断点或几个日志。
也许我们会看到theme
是错误的但width
和isMobile
是正确的。这就是告诉我们问题在useTheme()
。或者也许我们会看到width
本身是错误的。我们将去检查useWindowWidth()
。
只要看一下中间值,就会知道顶层的哪个Hooks包含这个bug。 我们不需要查看他们的所有实现。
然后我们可以“放大”有错误的那个,并复现。
如果自定义Hook嵌套的深度增加,这变得更加重要。想象一下,我们有3个级别的自定义Hook嵌套,每个级别使用3个不同的自定义Hooks。寻找 3个地方 的错误与可能检查 3 + 3×3 + 3×3×3 = 39个地方 之间的[差异](difference)是巨大的。幸运的是,useState()
不能神奇地“影响”其他钩子或组件。它返回的错误值在后面留下了一个痕迹,就像任何变量一样。🐛
结论: ✅useState()
不会让我们代码中的因果关系模糊不清。
不是一个Hooks: useBailout()
useBailout()
作为优化,使用Hooks的组件可以避免重新渲染。
有一种方法是为整个组件包裹一个React.memo()
包装器。如果props和上次渲染的props浅比较相等,那么就会避免重复渲染。他就像类中的PureComponent
。
React.memo
接受一个组件并且返回组件:
但为什么它不只是一个Hook?
无论你将其称为useShouldComponentUpdate()
,usePure()
,useSkipRender()
或useBailout()
,提案往往看起来像这样:
还有一些变化(例如简单的usePure()
标记)但从广义上讲,它们也存在同样的缺陷。
组合
假设我们尝试将useBailout()
放在两个自定义Hook中:
现在如果你在同一个组件中使用它们会发生什么?
什么时候重新渲染?
如果每个useBailout()
调用都有权跳过更新,那么useWindowWidth()
的更新将被useFriendStatus()
阻止,反之亦然。这些Hook会相互破坏。
然而,如果仅当在单个组件中对useBailout()
的所有调用都“同意”阻止更新时,我们的ChatThread
将无法更新对isTyping
prop的更改。
更糟糕的是,使用这些语义,如果他们不调用useBailout()
,任何新添加到ChatThread
的Hook都会中断。 否则,他们不能在useWindowWidth()
和useFriendStatus()
内“反对”(阻止)。
结论: 🔴useBailout()
打破了组合。将其添加到Hook会破坏其他Hook中的状态更新。我们希望 api 是反脆弱的, 这种行为几乎是相反的。
调试
像usehelout()
这样的Hook如何影响调试?
我们将使用相同的示例:
假设Typing...
标签在我们预期时没有出现,即使在props上方的许多层正在发生变化。我们如何调试它?
通常,在React中,你可以通过向上查找来自信地回答这个问题。 如果ChatThread
没有获得新的isTyping
值,我们可以打开渲染<ChatThread isTyping = {myVar} />
的组件并检查myVar
,依此类推。在其中一层,我们要么找到一个错误的shouldComponentUpdate()
,要么传递的isTyping
值不正确。对链中的每个组件进行一次检查通常就足以找到问题的根源。
但是,如果这个useBailout()
Hook是真实的,那么在你检查我们的ChatThread
和其所有者链中的组件所使用的每个自定义Hook(深度)之前,你永远不会知道更新被跳过的原因。由于每个父组件也可以使用自定义Hook,因此这种情况非常严重。
就像你在抽屉里寻找一把螺丝刀一样,每个抽屉里都装着一堆较小的抽屉柜,你不知道这个洞有多深。
结论: 🔴不仅useBailout()
Hook打破了组合,而且它也大大增加了调试步骤的数量和认知负荷,以便找到一个错误 - 在某些情况下,指数级。
我们只看了一个真正的Hook,useState()
,以及一个故意不是Hook的常见建议 - useBailout()
。我们通过组合和调试对它们进行了比较,并讨论了为什么其中一个工作而另一个不工作。
虽然没有memo()
或shouldComponentUpdate()
的“Hook版本”,但React确实提供了一个名为useMemo()的Hook。它有类似的用途,但它的语义不同,不会遇到上述陷阱。
useBailout()
只是一个不如Hook那么好的作业的例子。但还有其他一些 - 例如,useProvider()
,useCatch()
或useSuspense()
。
你能明白为什么吗?
(悄悄话:作曲......调试......)
Last updated