React Hooks 梳理

1,594 阅读4分钟

自 React 16.8 发布以后,在已有项目中,把 package.json 中的 react 和 react-dom 版本一升,就可以抄起 Hooks 开干了。笔者目前已经在项目中开始了实操,但不妨先总结下官方文档中一些值得梳理的点。

useState

为什么 useState 不叫 createState 呢?

  • 初始渲染时,useState 返回的是 initState
  • 下次渲染时,useState 返回的是 curState

也就是说,create 的叫法就不太符合初始渲染之后获取到的是「当前状态」这么一个事实了。

为什么 useState 不通过 this 也知道自己是哪个 Component 的状态?

每个组件内部都有一个「内存格子」的列表,他们就是一些存放数据的 JS 对象,当我们使用如 useState 的 Hooks 时,就会去读取当前的格子(或者在初始渲染的时候进行初始化),然后将指针移动到下一个 Hooks。这就是为什么一个组件内部的多个 useState 都能获取到各自的局部状态。

但是需要注意的是,这也是为什么官方建议我们要将 hooks 的调用顺序保持一致

useEffect

和过去的生命周期有什么区别?

其一,React 会在每次渲染完成后会调用 useEffect,如果使用传统的生命周期钩子的话,当我们希望每次 render 后执行某种副作用时,我们不得不在 componentDidMount 和 componentDidUpdate 里都塞上相同的逻辑,带来冗余。因此,传统的生命周期是不能代替 useEffect 的。这一点可参考 React Class 生命周期

当然,相比较考虑 mount 和 update,只考虑 render 是要简单清晰不少。

其二,Hooks 让我们可以基于逻辑而拆分代码,而不是基于生命周期。这一点非常重要,因为基于生命周期来拆分代码,势必让逻辑相关联的代码分散各处。使用 Hooks,我们就可以按照我们指定的顺序使用每一个副作用。

传入的函数每次 render 都是新的?

是的,这是为了保证在 useEffect 中使用到的内部状态都是最新的。这样 useEffect 就很像是 render 的一部分了 —— 每次使用的 useEffect 都属于其对应的的 render。

不仅如此,我们在 useEffect 中 return 的方法,也即通常用来做取消订阅这类 cleanup 工作的,每次 render 后也都会执行一次新的副作用(准确的说会先走 return 的方法,再重新走一次 useEffect 中的方法),而绝不是 unmount 的时候才执行一次。这种模式会有更少的 bug。

什么样的 bug 呢?可以看官方文档的例子,大致就是说,如果我们订阅的人的 id 变了,就需要取消订阅然后重新订阅新的人。这样一来,如果在使用 class 做订阅这类处理时,就需要在 3 个生命周期(componentDidMount、componentDidUpdate、componentWillUnmount)里散布逻辑,即在 componentDidUpdate 补充上取消并重新订阅的逻辑!

如果用了 useEffect,这些东西根本不需要去考虑。整个过程如文档中给的例子一样依次执行:

function FriendStatus(props) {
  // ...
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}

// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // Run first effect

// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // Run next effect

// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // Run next effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect

useEffect 第二个参数的优化作用

对 return 的 cleanup 同样适用,不要忘了,每次 render 完就会先执行一次 cleanup,最终 unmount 的时候也会执行一次 cleanup。

useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // 只会在 props.friend.id 变化的时候重新订阅

如果我们不提供该参数,每次更新都会重新执行;如果只想 mount 和 unmount 的时候各执行一次,可指定 [],但这不是好的实践方式,考虑到 useEffect 都是在 render 完后执行的,多做点工作可能会少点问题。

Hooks 使用原则

Only Call Hooks at the Top Level. Don’t call Hooks inside loops, conditions, or nested functions.

这一条的原因是,Hooks 是通过调用顺序分配存放位置的,只有每次 run 的时候顺序保持一致,才能挨个取得正确的 useState、useEffect。比方说,如果我们把 Hooks 放到条件语句里,然后第一次 render 的时候每个都执行,第二次 render 却有一个 Hook 不执行,那么后面的对应就出错了。很好理解吧。

但如果我们一定要有条件的执行 useEffect 呢?我们可以在 useEffect 内部加条件

  useEffect(function persistForm() {
    // 👍 这样就不会破坏第一条原则
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
  });

Only Call Hooks from React Functions.

这条没什么说的,总之只在下面两处用 Hooks:

  • ✅ Call Hooks from React function components.
  • ✅ Call Hooks from custom Hooks.