Hilshire / blog

temporary blog
2 stars 0 forks source link

react 文档笔记-高级指引部分 #43

Closed Hilshire closed 4 years ago

Hilshire commented 4 years ago

无障碍

跳过

代码分割

  1. 使用动态 import 时,webpack将自动进行分割;
  2. React.lazy()Suspense 配合动态分割,可以让我们在动态加载的时候,显示更友好的内容 用法:用React.lazy() 包裹import,与Suspense组合,同时在Suspensefallback里写入加载时的内容。
    
    import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent')); const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

function MyComponent() { return (

Loading...
}>

); }


## Context
用于在多个组件之间共享的内容。
这个很容易就会让人想起那一堆数据管理框架,这些的功能看上去有些重叠。但是两者的应用场景其实不太一样。总的来说,对 `context` 的使用应该更谨慎一点。

> Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言

> Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。
> 如果你只是想避免层层传递一些属性,组件组合(component composition)有时候是一个比 context 更好的解决方案

文档中提到,如果组件的嵌套层数过深,导致一些只有深层组件需要的值需要层层传递,那么可以把组件本身作为`prop`传下去

需要注意的是,这种做法依然要层层传递你提取的组件。但是,它确实更加解耦了一些。至少当这个组件需要更多父组件的属性的时候,不需要修改那些层层传递的 `props`

我并不认为这个能对数据管理框架造成什么样的影响。如果一个项目用 `context` 就可以完成管理,那么它一开始其实就不太需要专门的数据管理框架。现在,人们也不能因为怕麻烦就去引入`redux`了,这个理由在`context`面前不成立。项目中滥用数据管理框架的问题,我想许多负责人都遇到过,每个人都有一堆牢骚要发。

- React.createContext
创建
`const MyContext = React.createContext(defaultValue);`
- Context.Provider
注入
`<MyContext.Provider value={/* 某个值 */}>`
- Class.contextType
```jsx
class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
  }
}
MyClass.contextType = MyContext;

错误边界

组件渲染抛出异常的时候,显示准备好的错误信息。 事件内的异常不会在这里处理(不过如果事件导致组件异常,会怎么样呢?)

自 React 16 起,任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载。


class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) { // 更新 state 使下一次渲染能够显示降级后的 UI return { hasError: true }; }

componentDidCatch(error, errorInfo) { // 你同样可以将错误日志上报给服务器 logErrorToMyService(error, errorInfo); }

render() { if (this.state.hasError) { // 你可以自定义降级后的 UI 并渲染 return

Something went wrong.

; }

return this.props.children; 

} }

## Refs 转发
> Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。

我个人更喜欢 `react` 的原因之一,就是它可以更好的写一些包装组件。比如它可以很轻松的透传 `props`,又比如说 Ref 转发。

```jsx
const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

Fragments

Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。 这在处理表格的时候非常有用。它还有一个简写:

class Columns extends React.Component {
  render() {
    return (
      <>
        <td>Hello</td>
        <td>World</td>
      </>
    );
  }
}

很 cool ,不是吗?

Hilshire commented 4 years ago

高阶组件(HOC)

HOC有些难解释,因为这不是 api ,而是 react 的一种设计模式。无论是名字还是用法,HOC 都和高阶函数有些相似。如果你对高阶函数有足够的了解,并且经常在项目中使用的话,想必会对HOC有这更好的理解。

好在 HOC 是一个一旦掌握了就不会忘记的技能。实际上,我更愿意称呼它为某种『习惯』或者『范式』,它本质提供了一种代码抽象的手段。

使用HOC要注意以下几点:

  1. 不要再HOC中修改组件原型。 这是理所当然的要求。一旦修改了原型,就意味着你的组件在多次渲染的时候一定是不可靠的。
  2. 约定:将不相关的 props 传递给被包裹的组件 简单来说,props应该透传给被包裹的组件。这也很好理解。这样做保证了可以灵活替换包装前的组件和包装后的组件。
  3. 约定:最大化可组合性 这个约定解释起来就有点麻烦...

    // React Redux 的 `connect` 函数
    const ConnectedComment = connect(commentSelector, commentActions)(CommentList);

    这个函数大家应该都不陌生,HOC 应该符合 Component => Component 这样的函数签名。这让我们可以向上面一样组合HOC。对于一些情况,可以用 compose 处理

    // 而不是这样...
    const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))
    
    // ... 你可以编写组合工具函数
    // compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
    const enhance = compose(
      // 这些都是单参数的 HOC
      withRouter,
      connect(commentSelector)
    )
    const EnhancedComponent = enhance(WrappedComponent)
  4. 约定:包装显示名称以便轻松调试 显示名称应该为 WithSubscription(CommentList):

    function withSubscription(WrappedComponent) {
      class WithSubscription extends React.Component {/* ... */}
      WithSubscription.displayName = 
      `WithSubscription(${getDisplayName(WrappedComponent)})`;
      return WithSubscription;
    }
    
    function getDisplayName(WrappedComponent) {
      return WrappedComponent.displayName || WrappedComponent.name || 'Component';
    }
  5. 不要在 render 方法中使用 HOC
    render() {
      // 每次调用 render 函数都会创建一个新的 EnhancedComponent
      // EnhancedComponent1 !== EnhancedComponent2
      const EnhancedComponent = enhance(MyComponent);
      // 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!此外还会丢失状态
      return <EnhancedComponent />;
    }
  6. 务必复制静态方法 也很好理解,本质依然是为了灵活性。 当然,这样写出来的代码很丑,所以官方给出了解决方案: hoist-non-react-statics
  7. Refs 不会被传递 老话题了,forwardRef 解决之
Hilshire commented 4 years ago

与第三方库协同

很长的一章,然而其实没什么内容。这些就够了:

class SomePlugin extends React.Component {
  componentDidMount() {
    this.$el = $(this.el);
    this.$el.somePlugin();
  }

  componentWillUnmount() {
    this.$el.somePlugin('destroy');
  }

  render() {
    return <div ref={el => this.el = el} />;
  }
}

深入JSX

比较杂,不重要的部分省略了。

  1. React 必须在作用域内 因为jsx会被编译成 React.createElement(...)
  2. 在 JSX 类型中使用点语法 看过一次就不会忘。<MyComponents.DatePicker color="blue" />
  3. 用户定义的组件必须以大写字母开头 yes, sir!
  4. 在运行时选择类型
    
    import React from 'react';
    import { PhotoStory, VideoStory } from './stories';

const components = { photo: PhotoStory, video: VideoStory };

function Story(props) { // 错误!JSX 类型不能是一个表达式。 return <components[props.storyType] story={props.story} />; }

```jsx
import React from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
  photo: PhotoStory,
  video: VideoStory
};

function Story(props) {
  // 正确!JSX 类型可以是大写字母开头的变量。
  const SpecificStory = components[props.storyType];
  return <SpecificStory story={props.story} />;
}
  1. Props 默认值为 “True” 不建议不传 value 给 prop

  2. 属性展开

性能优化

  1. 使用 Chrome Performance 标签分析组件
  2. 使用开发者工具中的分析器对组件进行分析
  3. 虚拟化长列表 提到了两个插件:react-window react-virtualized
  4. shouldComponentUpdate

    如果你知道在什么情况下你的组件不需要更新,你可以在 shouldComponentUpdate 中返回 false 来跳过整个渲染过程。其包括该组件的 render 调用以及之后的操作。

  5. PureComponent

    在大部分情况下,你可以继承 React.PureComponent 以代替手写 shouldComponentUpdate()。它用当前与之前 props 和 state 的浅比较覆写了 shouldComponentUpdate() 的实现。

疑问: pureComponent 为什么可以加快性能?react 会在什么时候更新组件?

Hilshire commented 4 years ago

Portals

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。 值得注意的是,这种情况下,组件在组件树中的位置是不变的。我认为这种不对应可能会造成困惑。 react 团队显然也知道这一点。文档上有专门的一段用来写这种情况下的冒泡行为。及时DOM再不同的地方,事件依然会冒泡到父组件上。

Profiler API

Profiler 测量渲染一个 React 应用多久渲染一次以及渲染一次的“代价”。 它的目的是识别出应用中渲染较慢的部分,或是可以使用类似 memoization 优化的部分,并从相关优化中获益。

Profiler 包含两个 prop:一个是 id,另一个是 onRender

function onRenderCallback(
  id, // 发生提交的 Profiler 树的 “id”
  phase, // "mount" (如果组件树刚加载) 或者 "update" (如果它重渲染了)之一
  actualDuration, // 本次更新 committed 花费的渲染时间
  baseDuration, // 估计不使用 memoization 的情况下渲染整颗子树需要的时间
  startTime, // 本次更新中 React 开始渲染的时间
  commitTime, // 本次更新中 React committed 的时间
  interactions // 属于本次更新的 interactions 的集合
) {
  // 合计或记录渲染时间。。。
}
Hilshire commented 4 years ago

不使用 ES6

不使用 JSX

协调(diff)

名字起的很高大上,内容实际也很高大上,但是总有点文不对题的感觉——往下面一翻,就发现突然开始讲解起diff算法了。于是又理所当然地解释了一遍 react 是如何把 O(n3) 的复杂度变成 O(n) 的。而这种算法可以实现,源自两个前提:

  1. 两个不同类型的元素会产生出不同的树;
  2. 开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定;

而上述前提成立的理由,fb给出的答案是:

在实践中,我们发现以上假设在几乎所有实用的场景下都成立。

在我看来,这个答案是很让人不安的。当然,抓着这一点不放颇有吹毛求疵之嫌。

diff算法

  1. 对比不同类型元素:重新渲染(state也会被销毁)
  2. 对比同一类型元素:仅比对及更新有改变的属性
  3. 比对同类型的组件元素:

    当一个组件更新时,组件实例保持不变,这样 state 在跨越不同的渲染时保持一致。React 将更新该组件实例的 props 以跟最新的元素保持一致,并且调用该实例的 componentWillReceiveProps()componentWillUpdate() 方法。

    注:在 react16 中,componentWillReceiveProps 已经不再推荐使用。

  4. 对子节点进行递归

    在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation。 在子元素列表末尾新增元素时,更变开销比较小 但是,在元素列表头部新增元素,开销会变大。所以,react变更每一个元素来实现在头部新增元素。当然,这样的开销也是不可接受的。这也就是为什么 react 的列表都要求一个 key

  5. 使用数组下标作为 key: 这是可行的。但是要小心两种情况:

    这个策略在元素不进行重新排序时比较合适,但一旦有顺序修改,diff 就会变得慢。

    当基于下标的组件进行重新排序时,组件 state 可能会遇到一些问题。由于组件实例是基于它们的 key 来决定是否更新以及复用,如果 key 是一个下标,那么修改顺序时会修改当前的 key,导致非受控组件的 state(比如输入框)可能相互篡改导致无法预期的变动

    结论很明显:能不用下标就不用下标,尤其是使用后端数据进行渲染的时候。

总结

请谨记协调算法是一个实现细节。React 可以在每个 action 之后对整个应用进行重新渲染,得到的最终结果也会是一样的。在此情境下,重新渲染表示在所有组件内调用 render 方法,这不代表 React 会卸载或装载它们。React 只会基于以上提到的规则来决定如何进行差异的合并。

我们定期探索优化算法,让常见用例更高效地执行。在当前的实现中,可以理解为一棵子树能在其兄弟之间移动,但不能移动到其他位置。在这种情况下,算法会重新渲染整棵子树。

由于 React 依赖探索的算法,因此当以下假设没有得到满足,性能会有所损耗。

  1. 该算法不会尝试匹配不同组件类型的子树。如果你发现你在两种不同类型的组件中切换,但输出非常相似的内容,建议把它们改成同一类型。在实践中,我们没有遇到这类问题。
  2. Key 应该具有稳定,可预测,以及列表内唯一的特质。不稳定的 key(比如通过 Math.random() 生成的)会导致许多组件实例和 DOM 节点被不必要地重新创建,这可能导致性能下降和子组件中的状态丢失。

Refs and the DOM

这一章节的开头,作者不厌其烦地声明不要滥用 ref ,以及 ref 应该被用在什么场景。事实证明,ref 确实很容易被滥用,毕竟人都是懒惰的,而中国被巨量的需求压榨的码农实在是太多了。

文档列出的,应该使用 ref 的情况有以下三种:

管理焦点,文本选择或媒体播放。 触发强制动画。 集成第三方 DOM 库。

不得不说这是一个非常严苛的要求。而这个要求的立足点在于,大部分情况下,不使用 ref 获取 DOM 的场景,其实都可以用状态提升解决。换句话说,如果有场景需要使用 ref 获得一个组件,那么很有可能是程序中某个地方出了问题,也就是我们俗称的『坏味道』。

不过,在实际开发中,实际上很难完全避免这一点。

创建ref

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return <div ref={this.myRef} />;
  }
}

访问ref

const node = this.myRef.current;

ref 的值根据节点的类型而有所不同:

当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。 你不能在函数组件上使用 ref 属性,因为他们没有实例。

ref 会在 componentDidMount 或 componentDidUpdate 生命周期钩子触发前更新。

回调ref

这个就有意思了:除了属性之外,我们也可以传递一个回调。

它能助你更精细地控制何时 refs 被设置和解除。 在 js 当中,函数的威力不言自明。然而这种灵活性当然也提供了 cheat 的可能 我不想粘贴实例里的那一坨代码,所以只附上我觉得重要的部分:


...
this.textInput = null;
this.setTextInputRef = element => {
  this.textInput = element;
};

this.focusTextInput = () => {
  // 使用原生 DOM API 使 text 输入框获得焦点
  if (this.textInput) this.textInput.focus();
};

...

文档中展示的小小魔术:
```jsx
function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />
    </div>
  );
}

class Parent extends React.Component {
  render() {
    return (
      <CustomTextInput
        inputRef={el => this.inputElement = el}
      />
    );
  }
}
Hilshire commented 4 years ago

Render Props

这是非常重要的一章。但是这一章其实没有新的API,而是阐述了一种 react 使用范式。这一范式是为了解决所谓的『横切关注点』问题,简单来说是为了处理多组件之间的逻辑复用问题。

提到这个首先想到的应该是 this.props.childrenHOC: 这两个都是 react 为了解决这个问题而提供的武器。实际上我想不出有什么是这两个解决不了,而需要 render props 解决的问题。如果我有一些页面需要复用,我会考虑用 children prop;如果我只是一些逻辑需要复用,那我就使用 HOC。那么问题来了,使用 Render Props 的场景是什么呢?

如过让我来选择,我会在同时需要复用视图和逻辑的时候使用它——实际上,这个说法也很狭隘,因为这三者其实是不冲突的。

我会用单独的一个 comment 来放这一节,不过其实没有什么东西想写。这个观念上的章节还是直接看文档更加合适一点。具体的代码文档用三行就展示了出来,尽管下面跟了很长篇幅的例子。

<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>

对了,还有一个需要注意的地方: 将 Render Props 与 React.PureComponent 一起使用时要小心

如果你在 render 方法里创建函数,那么使用 render prop 会抵消使用 React.PureComponent 带来的优势。因为浅比较 props 的时候总会得到 false,并且在这种情况下每一个 render 对于 render prop 将会生成一个新的值。

为了绕过这一问题,有时你可以定义一个 prop 作为实例方法,类似这样:

Hilshire commented 4 years ago

静态类型检查

略。 为什么这部分内容会出现在高阶指南里?我不是说它简单什么的,而是说这个难道不应该专门找个地方放着吗?

严格模式

react 有自己的严格模式,像这样开启:

import React from 'react';

function ExampleApplication() {
  return (
    <div>
      <Header />
      <React.StrictMode>
        <div>
          <ComponentOne />
          <ComponentTwo />
        </div>
      </React.StrictMode>
      <Footer />
    </div>
  );
}

开启之后可以帮你检查是不是用了过时的api之类的问题,不是什么有趣的内容。不过有一个地方很棒:

检测意外的副作用

渲染阶段的生命周期包括以下 class 组件方法:

constructor componentWillMount (or UNSAFE_componentWillMount) componentWillReceiveProps (or UNSAFE_componentWillReceiveProps) componentWillUpdate (or UNSAFE_componentWillUpdate) getDerivedStateFromProps shouldComponentUpdate render setState 更新函数(第一个参数) 因为上述方法可能会被多次调用,所以不要在它们内部编写副作用相关的代码,这点非常重要。忽略此规则可能会导致各种问题的产生,包括内存泄漏和或出现无效的应用程序状态。不幸的是,这些问题很难被发现,因为它们通常具有非确定性。

如果我没有记错(这常常会发生),这好像是文档里第一次提到生命周期函数的副作用问题,而且还是在这样的一节里面。我觉得这不太应该。当然,这个可能没我想的那么重要,比较有副作用也不影响实现业务。可是如果这样,之前的一些范式也不影响实现具体业务啊。

对我而言这部分内容比严格模式来的有趣多了。

使用 PropTypes 进行类型检查

用到去查就是了。

但是这里面包含了一块内容来说明如何设定默认值:

您可以通过配置特定的 defaultProps 属性来定义 props 的默认值

class Greeting extends React.Component {
  render() {
    return (
      <h1>Hello, {this.props.name}</h1>
    );
  }
}

// 指定 props 的默认值:
Greeting.defaultProps = {
  name: 'Stranger'
};

// 渲染出 "Hello, Stranger":
ReactDOM.render(
  <Greeting />,
  document.getElementById('example')
);

不是,我寻思着前面讲 props 的时候也没提到这个呀?能不能不要把这种内容塞在这种犄角旮旯地方啊?

Hilshire commented 4 years ago

非受控组件

文档淡化了非受控组件的部分,可以看出 fb 不是很想让人去使用这一种技术,以至于都不太想让人知道这是一种什么东西。

不过,我认为受控组件和非受控组件是非常重要的内容。对这一部分的理解可以看出一个开发对组件化开发的理解。而非受控组件很容易引发bug,因为数据流在这里被截断了。理解两者的区别可以让你对这种情况变得敏感,虽然进行过一段时间开发之后,人们往往会习惯写受控组件(这也是 fb 希望看到的结果)。

在一个受控组件中,表单数据由 DOM 管理,这意味着你不需要为 DOM 写 change 回调。这会减轻我们的代码量,而且某种意义上因为更贴近传统开发模式,有的时候更方便集成一些东西——它的好处仅此而已。

此外 react 为这些表单提供了 defaultValue 属性用来给予默认值。

这一块虽然内容很少,我依然用一个 comment 来放它。这是我的个人偏好。

Hilshire commented 4 years ago

Web Components

略。我想不出任何需要同时使用两者的场景。

我选择单独用一个comment放这一部分,因为这是高级指引的最后一部分了🤣

Hilshire commented 4 years ago

React API

大部分情况下,框架提供的API都比普通业务需要的API多。所以,即使是工作事件很长的开发,也会有不了解的API,这也是我不太喜欢一直问人冷门API的人的原因。

当然,要是什么都不知道那肯定是不行的。

只列出我关心的,且文档较少提到的api

React

  1. React.memo

    React.memo 为高阶组件。它与 React.PureComponent 非常相似,但只适用于函数组件,而不适用 class 组件。

    const MyComponent = React.memo(function MyComponent(props) {
    /* 使用 props 渲染 */
    });

    默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。 此方法仅作为性能优化的方式而存在。但请不要依赖它来“阻止”渲染,因为这会产生 bug。

  2. cloneElement() 不算很有用
    React.cloneElement(
      element,
      [props],
      [...children]
    )
  3. isValidElement()
  4. React.Children
    React.Children.map(children, function[(thisArg)])

    ReactDOM

  5. hydrate() 这个函数参考这里: react中出现的"hydrate"这个单词到底是什么意思?
  6. unmountComponentAtNode()

    ReactDOMServer

    服务端渲染相关,略

    DOM属性差异

    建议看文档

  7. checked
  8. className
  9. dangerouslySetInnerHTML
  10. htmlFor
  11. onChange
  12. selected
  13. suppressContentEditableWarning
  14. suppressHydrationWarning
  15. value

    合成事件

    react 对事件进行了处理,你获得的实际是 SyntheticEvent 对象。

    SyntheticEvent 实例将被传递给你的事件处理函数,它是浏览器的原生事件的跨浏览器包装器。除兼容所有浏览器外,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation()preventDefault()。 如果需要获得原生事件对象,可以访问nativeEvent 属性。

    事件池

    SyntheticEvent 是合并而来。这意味着 SyntheticEvent 对象可能会被重用,而且在事件回调函数被调用后,所有的属性都会无效。出于性能考虑,你不能通过异步访问事件。

这句话基本上等于在说:『react 事件有自己的机制,但我不打算在这里详说,你要是想了解可以自己去研究。』

注意:

如果你想异步访问事件属性,你需在事件上调用 event.persist(),此方法会从池中移除合成事件,允许用户代码保留对事件的引用。

之后时一个长长的事件列表,略过不表

test 相关

略 我知道 test 很重要啦,实际上每个人都知道 test 很重要,但是目前我还没在视图相关业务中看到过测试。

术语表

略。没有新东西。

Hilshire commented 4 years ago

hook相关

这也是一块非常难写的部分。文档中有一半是 hook 的理念,还有一半是 hook 的用法。前者不好写,后者没必要。更过分的是,一读文档,立刻发现有一些东西暗藏在那些激动人心的叙述里面,文档如同冬天的湖面上冷澈的薄冰。

从前面的高级指引,我们可以很轻松的看出来,组件之间逻辑的复用是前段组件化编程的痛点。尽管提供了 HOC 和 render props , react 对这样的解决方案还是不太满意。另一方面,react团队 对函数式编程的热爱深入骨髓,完善函数组件的动力显然非常充足。

这一点是由 JS 的特性决定的。与其说 react 喜欢函数,其实不如说 JS 喜欢函数。对 JS 而言,逻辑复用最方便的方法永远是函数。那么扩展函数组件就成为了理所当然的选择。

同时,它带来了生命周期函数的震荡。当我们用函数的思维来思考的时候,副作用的概念是那么的理所当然,而 MVVM 下任何副作用似乎都可以归结为视图的变化,这样想来 useEffert 的存在就显得非常理所当然,仿佛它本来就应该在那里似的。当我们用这种方式思考的时候,生命周期函数就被踢到了一边,一切都是状态和副作用。

创建自定义hook极其简单,而自定义hook本质上也只是使用了 use 开头的函数而已。本质上是对逻辑的封装。

// 文档中的自定义hook
import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

这样看来,事情似乎变得简单了。样板代码变得很少,逻辑复用也很灵活。即使JS的函数意味着最大程度的灵活性,react 显然也要做一些事情去整合这种写法的状态。一个只提供状态,不渲染任何视图的函数组件,它内部的 state 和使用它的组件到底是什么关系?它有一个自己的副本吗?还是说相当于在父函数内调用了 useState?更别说官方给出的限制了:

只在最顶层使用 Hook 不要在循环,条件或嵌套函数中调用 Hook

只在 React 函数中调用 Hook

这些镣铐或多或少向你暗示有某些暗坑潜藏在冰面之下,这种情况下的 hook 只能做为一种进阶用法存在。可能使用 hook 写出来的项目确实更解耦,更好维护,但是如果有某个不谙世事的新人乱搞一气,可能事情就会变得麻烦起来——不过,什么样的代码不是这样呢?

事情不只是这样简单而已。再往下看看,会发现 react 还提供了 useConetxtuseRenducer 两个 hookuseContext 很正常,这是一个必要的功能,useReducer 则不同,无论怎么看都觉得是在冲 redux 开炮。尽管目前只是一个简单的玩具,但是 redux 的必要性又进一步降低了。react 也许不会提供数据管理功能,但是可以提供部分能力,压缩需要数据管理的情况。毕竟redux 实在不是什么轻量级的框架。