如何评价 React v16.7.0-alpha 提出的 Hooks API?

Introducing Hooks – React
关注者
952
被浏览
105,827

18 个回答

有幸今年暑假在 React team 实习了三个月,这个 API 一路走来到今天在 React Conf 上宣布了 16.7.0 的 hooks,以我个人(局限)的眼光谈一下这个 API 出现的原因和影响。

Hooks 这个概念从今年的四五月份就开始酝酿,最初的原型从设计 function component 怎样使用 state 开始,经历了各种讨论,十月末发布,可以说这个构思并不是大家突发奇想而来,而是真正解决了 Facebook 内部 React 构写应用的一系列问题。

这个回答不侧重于描述 hooks “是什么”以及“怎么用”,关于这两个问题,可以阅读官方文档以及我写的这篇入门文章。

这里我主要就“动机”以及“解决了什么真实问题”这个角度回答。

  1. 组件树过于臃肿

在一个大如 facebook.com 的应用里,任何可复用的组件都可能隐藏着大量的逻辑。就拿 Image 这个 component 来说,当你调试应用打开 React devtool 的时候,单独一个图片组件可能是这样的:

<ImageModalTextual>
  <ImageLayer>
    <CoreImage>
      <FooImageWrapper>
        <BarImageWrapper>
          <EncryptedImage>
            <RealImage />
          </EncryptedImage>
        </BarImageWrapper>
      </FooImageWrapper>
    </CoreImage>
  </ImageLayer>
</ImageModalTextual>

这其中大量的 wrapper 只是一些中间步骤抽象出来的可复用逻辑单元,而与真正的图片 UI 渲染无关。这个问题导致开发和调试出现了大量的问题。新的 feature 该加在哪里?是不是又要再加一个 wrapper 处理另一个新出现的问题?Bug 出现在中间的哪一环节?很多问题都极大地提高了开发成本。但是 hooks 的出现,使得一些逻辑复用独立于 React 组件树成为独立的单元,而不再在组件树中出现,这个问题就随之解决。

另一个例子是我们熟悉的 Context API。Context API 目前使用了 render props 这一个向下传递 context 的方式,这同样会造成不光组件树,乃至整个 render 逻辑的复杂化:

<FooContext.Consumer>
  {foo => (
    <BarContext.Consumer>
      {bar => (
        <BazContext.Consumer>
          {baz => (
            <Component />
          )}
        </BazContext.Consumer>
      )}
    </BarContext.Consumer>
  )}
</FooContext.Consumer>

用了十行代码,我们最终才把这几个 context 传下去,render props 导致代码可读性下降。但是用 hooks 的情况下,我们只需要在使用对应 context 的情况下,用一行 useContext 就得以解决:

const fooContext = useContext(FooContext);

这一点最直观的影响就是 “HOC 和 render props 作为中间组件”这种模式很可能将慢慢退出历史舞台。比如说 Recompose。

值得一提的是,正是由于 custom hook 独立于 UI 的这个特性,整个社区将(很有可能)出现大量的 custom hooks 供开发者调用。从这方面来讲,React 和社区接合的部分变得更加合理。

2. JavaScript's Class confuse both computer and human

在学习 JS 的过程中永远避不开 this 这个问题,而这个问题在 class component 中,结合各种回调函数又变得尤其复杂。早在很久以前我们就会在 constructor 里手动 bind 事件的回调函数:

this.handleInputChange = this.handleInputChange.bind(this);

后来我们又有了箭头函数的写法:

handleInputChange = () => {}

但是无论如何,class component 中的 this 一直是 bug 的重要来源之一。有了 Hooks,function component 不需要再使用 this。这也在一定程度上降低了开发者学习 React 的门槛。

3. Life-cycles 的问题

生命周期是一些业务逻辑复杂的组件绕不开的一环。但是随着逻辑变复杂,这些生命周期也随之变得很难维护和理解,比如说 componentDidMount, componentDidUpdate, componentWillUnmount 这些环节的一些 side effect 的一些相互作用到底是怎样配合 react 的渲染工作的?这些对即使有经验的开发者也可能是一个不小的挑战。由此,一个组件在多人的维护下,可能会慢慢变得非常复杂(没人敢删代码)。

Hook 在最开始使用 useLifeCycle 这个钩子,但是随后抛弃了这个概念而使用了 effect 的概念将所有的 side effect 聚合在这里。通过 useEffect,我们可能将以前的多个 life-cycles 合并、重组,使逻辑更加清晰。具体例子请看 Using the Effect Hook – React


以上这几点是我个人在实习过程以及看完文档后觉得最有共鸣的关于 Hooks 的影响。今天正好是 React Conf 的第一天,team 前几天刚宣布了 16.6 Suspense API 的情况下,又抛出来 Hooks,整个社区都像炸了一样。大家都有很多很棒的点子。有一些工具,比如说 Recompose 将退出历史舞台,HOC 和 render props 这两个 pattern 可能越来越少。

关于学习 Hooks 以及对这个问题更好更全面的回答,只推荐官方文档,这些文档是通过 team 里最顶尖的工程师不断润色修改写出来的,对每个词的斟酌都十分讲究。

很高兴能看到曾经在 FB 内部参与多轮迭代讨论的 API 终于能够面向所有的开发者,不再成为最开始只是 team 里几个人的秘密。Happy writing React!

就酱。


更新:React Conf 关于 Hooks 的 talk 在 youtube 上更新了,推荐大家去看:

周末简单研究了一下 hook 的形态,一些 motivation、optimization、concept 大家都提了,所以就简单的聊一个相关的场景:


一直以来我都很困扰如何把 rxjs 和 react 更好的结合在一起。 两者虽然都一定程度在表达 reactive 的概念 (reactive ui / reactive dataflow 它们的好处,我也不在此多做赘述),然而每当我们想同时使用它们的时候,发现没办法更自然结合两者。私下里,我试过很多方法,比如实现一个自己的基于流的 Component 类,或者做一些动态的修改,虽然一定程度上可以减少噪音,但这些封装于我而言,始终不够满意,有着这样那样、或多或少的缺陷。


在抛开 redux-observable / mobx 的语境下,使用 rxjs, 我们需要在 didMount 的时候 subscribe,并且牢记在 unmount 的时候需要 unsubscribe。诚然,在这对 lifecycle 的帮助下,我们可以很好的、正常的使用流的概念,可当我们把视角提高到系统设计的角度时,对于视图 (Component) 的设计者/消费者而言,数据的形态真的是重要的么? 对组件而言,它的职责是完成 Data -> View 的映射,但由于我们的数据与组件的 lifecycle 形成了直接的依赖关系,则意味着我们不得不把流的概念泄露到组件层,一定意义上,就把数据的表现形态耦合进了视图。


对于流的表达,angular 做的很好,得益于它们的编译器使得其向模板暴露了 async pipe 的语法,可以以一个极低的成本在高维的数据与模板之间做连接。


而随着 react 团队发布了 hook api,它不仅对组件频繁使用的 lifecycle 做了更通用的抽象 (减少 boilerplate 代码),更重要的是提供了一种“异步值的同步表达”。而得以于此,高阶的数据流可以被降维成普通的一阶数据,直接抹平了视图层上对由于数据形态不同导致的差异 —— 对视图而言,再也不需要知道什么是 Observable、Subscription 了 (i.e. unimportant detail to users) ,Effect 成为了一种新的抽象分层。总而言之,hook 不仅达到了实现细节的分离,还隔离了认知边界,降低了心智负担。


P.S.:

要说缺点,也是有的。

hook 是 react 的 eval
  • hook 虽然是一个 function,但最好别当它是一个 Javascript function,更倾向于是一个 react function,它的本质相对于 Javascript 是不自然的
  • hook 和函数式组件相依相随,是严格的伴生关系



// resizer.ts

import { Observable } from 'rxjs/Observable'

const resize$ = Observable.fromEvent(window, 'resize')
  .debounceTime(100)
  .map(() => [window.innerWidth, window.innerHeight])
  .shareReplay(1)

// useResizer.ts

import { useEffect, useState } from 'react'
import { resizer$ } from './resizer'

export function useResizer() {
  const [ value, setValue ] = useState([window.innerWidth, window.innerHeight])

  useEffect(() => {
    const subscription = resizer$.subscribe(setValue)
    return () => subscription.unsubscribe()
  }, [])

  return value
}

// app.ts

import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { useResizer } from './useResizer'

export function Example() {
  const [ width, height ] = useResizer()

  return (
    <div>
      <p>{ `Height: ${ height }` } </p>
      <p>{ `Width: ${ width }` } </p>
    </div>
  )
}

ReactDOM.render(<Example />, document.querySelector('#root') )