[译] X 为啥不是 hook?

由读者翻译的版本:西班牙语html

React Hooks 第一个 alpha 版本发布以来, 这个问题一直被激烈讨论:“为何 API 不是 hook?”前端

你要知道,只有下面这几个算是 hooks:react

可是像 React.memo()<Context.Provider>,这些 API 它们不是 Hooks。通常来讲,这些 Hook 版本的 API 被认为是 非组件化反模块化 的。这篇文章将帮助你理解其中的原理。android

注:这篇文章并不是教你如何高效的使用 React,而是对 hooks API 饶有兴趣的开发者所准备的深刻分析。ios


如下两个重要的属性是咱们但愿 React 的 APIs 应该拥有的:git

  1. 可组合Custom Hooks(自定义 Hooks)极大程度上决定了 Hooks API 为什么如此好用。咱们但愿开发者们常用自定义 hooks,这样就须要确保不一样开发者所写的 hooks 不会冲突。(撰写干净而且不会相互冲突的组件实在太棒了)github

  2. 可调试:随着应用的膨胀,咱们但愿 bug 很容易被发现。React 最棒的特性之一就是,当你发现某些渲染错误的时候,你能够顺着组件树寻找,直到找出是哪个组件的 props 或 state 的值致使的错误。后端

有了这两个约束,咱们就知道哪些算是真正意义上的 Hook,而哪些不算。api


一个真正的 Hook: 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 是错的,可是 widthisMobile 是对的。这会提示咱们问题出在 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。


它不是一个 Hook: 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 是稳定的,可是这个特性显然是与之相反了。

Debugging

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 TwitterEdit on GitHub

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索