- 原文地址:Why Isn’t X a Hook?
- 原文做者:Dan Abramov
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:Jerry-FD
- 校对者:yoyoyohamapi, CoolRice
由读者翻译的版本:西班牙语html
自 React Hooks 第一个 alpha 版本发布以来, 这个问题一直被激烈讨论:“为何 API 不是 hook?”前端
你要知道,只有下面这几个算是 hooks:react
useState()
用来声明 state 变量useEffect()
用来声明反作用useContext()
用来读取一些上下文可是像 React.memo()
和 <Context.Provider>
,这些 API 它们不是 Hooks。通常来讲,这些 Hook 版本的 API 被认为是 非组件化 或 反模块化 的。这篇文章将帮助你理解其中的原理。android
注:这篇文章并不是教你如何高效的使用 React,而是对 hooks API 饶有兴趣的开发者所准备的深刻分析。ios
如下两个重要的属性是咱们但愿 React 的 APIs 应该拥有的:git
可组合:Custom Hooks(自定义 Hooks)极大程度上决定了 Hooks API 为什么如此好用。咱们但愿开发者们常用自定义 hooks,这样就须要确保不一样开发者所写的 hooks 不会冲突。(撰写干净而且不会相互冲突的组件实在太棒了)github
可调试:随着应用的膨胀,咱们但愿 bug 很容易被发现。React 最棒的特性之一就是,当你发现某些渲染错误的时候,你能够顺着组件树寻找,直到找出是哪个组件的 props 或 state 的值致使的错误。后端
有了这两个约束,咱们就知道哪些算是真正意义上的 Hook,而哪些不算。api
useState()
多个自定义 Hooks 各自调用 useState()
不会冲突:安全
function useMyCustomHook1() {
const [value, setValue] = useState(0);
// 不管这里作了什么,它都只会做用在这里
}
function useMyCustomHook2() {
const [value, setValue] = useState(0);
// 不管这里作了什么,它都只会做用在这里
}
function MyComponent() {
useMyCustomHook1();
useMyCustomHook2();
// ...
}
复制代码
无限制的调用一个 useState()
老是安全的。在你声明新的状态量时,你不用理会其余组件用到的 Hooks,也不用担忧状态量的更新会相互干扰。
结论: ✅ useState()
不会使自定义 Hooks 变得脆弱。
Hooks 很是好用,由于你能够在 Hooks 之间传值:
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
// ...
return width;
}
function useTheme(isMobile) {
// ...
}
function Comment() {
const width = useWindowWidth();
const isMobile = width < MOBILE_VIEWPORT;
const theme = useTheme(isMobile);
return (
<section className={theme.comment}>
{/* ... */}
</section>
);
}
复制代码
可是若是咱们的代码出错了呢?咱们又该怎么调试?
咱们先假设,从 theme.comment
拿到的 CSS 的 class 是错的。咱们该怎么调试? 咱们能够打一个断点或者在咱们的组件体内加一些 log。
咱们可能会发现 theme
是错的,可是 width
和 isMobile
是对的。这会提示咱们问题出在 useTheme()
内部。又或许咱们发现 width
自己是错的。这能够指引咱们去查看 useWindowWidth()
。
简单看一下中间值就能指导咱们哪一个顶层的 Hooks 有 bug。 咱们不须要挨个去查看他们全部的实现。
这样,咱们就可以洞察 bug 所在的部分,几回三番以后,程序问题终得其解。
若是咱们的自定义 Hook 嵌套的层级加深的时候,这一点就显得很重要了。假设一下咱们有一个 3 层嵌套的自定义 Hook,每一层级的内部又用了 3 个不一样的自定义 Hooks。在 3 处找bug和最多 3 + 3×3 + 3×3×3 = 39 处找 bug 的区别是巨大的。幸运的是, useState()
不会魔法般的 “影响” 其余 Hooks 或组件。与任何 useState()
所返回的变量同样,一个可能形成 bug 的返回值也是有迹可循的。
结论: ✅ useState()
不会使你的代码逻辑变得模糊不清,咱们能够直接沿着面包屑找到 bug。
useBailout()
做为一个优化点,组件使用 Hooks 能够避免重复渲染(re-rendering)。
其中一个方法是使用 React.memo()
包裹住整个组件。若是 props 和上次渲染完以后对比浅相等(shallowly equal),就能够避免重复渲染。这和 class 模式中的PureComponent
很像。
React.memo()
接受一个组件做为参数,并返回一个组件:
function Button(props) {
// ...
}
export default React.memo(Button);
复制代码
但它为何就不是 Hook?
不论你叫它 useShouldComponentUpdate()
、usePure()
、useSkipRender()
仍是 useBailout()
,它看起来都差很少长这样:
function Button({ color }) {
// ⚠️ 不是真正的 API
useBailout(prevColor => prevColor !== color, color);
return (
<button className={'button-' + color}> OK </button>
)
}
复制代码
还有一些其余的变种 (好比:一个简单的 usePure()
) 可是大致上来讲,他们都有一些相同的缺陷。
咱们来试试把 useBailout()
放在 2 个自定义 Hooks 中:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ⚠️ 不是真正的 API
useBailout(prevIsOnline => prevIsOnline !== isOnline, isOnline);
useEffect(() => {
const handleStatusChange = status => setIsOnline(status.isOnline);
ChatAPI.subscribe(friendID, handleStatusChange);
return () => ChatAPI.unsubscribe(friendID, handleStatusChange);
});
return isOnline;
}
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
// ⚠️ 不是真正的 API
useBailout(prevWidth => prevWidth !== width, width);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
});
return width;
}
复制代码
译注:使用了 useBailout
后,useFriendStatus
只会在 isOnline
状态变化时才容许 re-render,useWindowWidth
只会在 width
变化时才容许 re-render。
如今若是你在同一个组件中同时用到他们会怎么样呢?
function ChatThread({ friendID, isTyping }) {
const width = useWindowWidth();
const isOnline = useFriendStatus(friendID);
return (
<ChatLayout width={width}>
<FriendStatus isOnline={isOnline} />
{isTyping && 'Typing...'}
</ChatLayout>
);
}
复制代码
何时会 re-render 呢?
若是每个 useBailout()
的调用都有能力跳过此次更新,若是 useFriendStatus()
阻止了 re-render,那么 useWindowWidth
就没法得到更新,反之亦然。这些 Hooks 会相互阻塞。
然而,在组件内部,假若只有全部调用了 useBailout()
都赞成不 re-render 组件才不会更新,那么当 props 中的 isTyping
改变时,因为内部全部 useBailout()
调用都没有赞成更新,致使 ChatThread
也没法更新。
基于这种假设,将致使更糟糕的局面,任何新置入组件的 Hooks 都须要去调用 useBailout()
,不这样作的话,它们就没法投出“反对票”来让本身得到更新。
结论: 🔴 useBailout()
破坏了可组合性。添加一个 Hook 会破坏其余 Hooks 的状态更新。咱们但愿这些 APIs 是稳定的,可是这个特性显然是与之相反了。
useBailout()
对调试有什么影响呢?
咱们用相同的例子:
function ChatThread({ friendID, isTyping }) {
const width = useWindowWidth();
const isOnline = useFriendStatus(friendID);
return (
<ChatLayout width={width}> <FriendStatus isOnline={isOnline} /> {isTyping && 'Typing...'} </ChatLayout> ); } 复制代码
事实上即便 prop 上层的某处改变了,Typing...
这个 label 也不会像咱们指望的那样出现。那么咱们怎么调试呢?
通常来讲, 在 React 中你能够经过向上寻找的办法,自信的回答这个问题。 若是 ChatThread
没有获得新的 isTyping
的值, 咱们能够打开那个渲染 <ChatThread isTyping={myVar} />
的组件,检查 myVar
,诸如此类。 在其中的某一层, 咱们会发现要么是容易出错的 shouldComponentUpdate()
跳过了渲染, 要么是一个错误的 isTyping
的值被传递了下来。一般来讲查看这条链路上的每一个组件,已经足够定位到问题的来源了。
然而, 假如这个 useBailout()
真是个 Hook,若是你不检查咱们在 ChatThread
中用到的每个自定义 Hook (深刻地) 和在各自链路上的全部组件,你永远都不会知道跳过此次更新的缘由。更由于任何父组件也可能会用到自定义 Hooks, 这个规模很恐怖。
这就像你要在抽屉里找一把螺丝刀,而每一层抽屉里都包含一堆小抽屉,你没法想象爱丽丝仙境中的兔子洞有多深。
结论:🔴 useBailout()
不只破坏了可组合性,也极大的增长了调试的步骤和找 bug 过程的认知负担 — 某些时候,是指数级的。
全文咱们探讨了一个真正的 Hook,useState()
,和一个不太算是 Hook 的 useBailout()
,并从可组合性及可调试性两个方面说明了为何一个是 Hook,而一个不算是 Hook。
尽管如今没有 “Hook 版本的 memo()
或 shouldComponentUpdate()
,但 React 确实提供了一个名叫 useMemo()
的 Hook。它有相似的做用,可是他的语义不会迷惑使用它的人。
useBailout()
这个例子,描述了控制组件是否 re-render 并不适合作成一个 hook。这里还有一些其余的例子 - 例如,useProvider()
, useCatch()
,useSuspense()
。
如今你知道为何某些 API 不算是 Hook 了吗?
(当你开始迷惑时,就提醒本身:可组合... 可调试)
Discuss on Twitter • Edit on GitHub
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。