React Hooks 的钩子够用吗?

蛋先生DX

68747470733a2f2f7373312e62647374617469632e636f6d2f37306346765853685f5131596e78476b706f574b314846366868792f69742f753d323336393934373531392c393134343033373226666d3d31352667703d302e6a7067.jpg

阅读此文前假设你对 React 的新特性 Hooks 有一定的了解,并能进行最基本的使用。如果还不清楚 React Hooks 是什么,也没关系,建议读完本文后再去阅读下官方关于 React Hooks 的文档

今天,我们要来聊一聊 React Hooks 官方当前提供的 useXXX 到底够不够用,是否能满足我们日常的开发需求。

先说说最重要的 State

: 喂喂,useState 这么核心的 API,不就是专门用于处理 State 的吗?

是的,一点都没错。用过 useState 的同学应该都知道,原来的 this.setState,现在可以用 useState 的返回值中的方法 setXXX 代替,如下所示:

const [ count, setCount ] = useState(0);

我们来简单对比一下 classHooks 写法的差别:

# get state

class: this.state.count
Hooks: count

# set state

class: this.setState({count: 1})
Hooks: setCount(1)
: 这个太简单了,了解过 React Hooks 的人都懂啊。所以你就是要给我讲这个?

当然,不是啦。我问你,有没有发现少了些什么呢?原来的 this.setState(),是不是有第二个参数?这第二个参数是不是一个callback方法?这个方法是不是会在状态成功更新后调用呢?

: 你这么说好像是哦,但callback我没怎么用,你能说下使用的场景吗?

当然,信手拈来。

联动选择下拉框的场景,比如城市选择器。当选择了省份后,就需要去获取对应的城市列表,这个 moment 就会用到了。话不多说,先来看下 setState 的例子:

class App extends React.Component {
  constructor() {
    this.state = {
      pId: 0
    };
  }

  onProvinceChanged(val) {
    this.setState({ pId: val }, () => {
      fetchCities(this.state.pId);
    });
  }
  
  ...
}

想想如果没有callback,我们就比较难准确地在状态更新后进行一些其它的操作了

: 所以 React Hooks 对此束手无策?我不信,我不信,我不信。

当然,不是啦。可以用 useEffect 来实现这个需求。我们来看下使用 Hooks 方式如何实现上面的功能

function App() {
  const [pId, setPId] = useState(0);

  useEffect(
    function() {
      fetchCities(pId);
    },
    [pId]
  );

  function onProvinceChanged(val) {
    setPId(val);
  }
}
: 那就是能实现咯,所以你接下来到底想说什么呢?

你仔细看下上面的代码,是不是很像事件的模式,一个在监听事件(useEffect 在监听 pId 的变化,然后执行方法)一个在触发事件(setPId)。

事件模式可以起到解耦代码的作用,但同时意味着代码很松散,一方只负责触发事件,而不用去关心接下来会发生什么。但我们这里的需求很明确,我选择了省份,接下来肯定是要加载城市数据,这两步的逻辑是有关联的,有先后顺序的,当然希望挨得越紧越好,这样代码比较有条理性,阅读起来比较顺畅,容易理解。不信的话,你就认真对比下上面的两个例子仔细推敲一下。

反正我是觉得还是得用回 callback 的方式,功能实现了固然重要,但代码的可读性,可维护性也很重要。

: 但你不是说 useState 没提供 callback 吗?那现在能怎么做?

当然,官方目前确实没有提供,但我们可以通过自定义 Hooks 的方式来实现。接下来我们会使用到第三方开源库 nice-hooks 来满足我们的需求

把上面的例子用 nice-hooks 提供的方法重新写一遍,直接上代码,非常简单:

import { useStateCB } from 'nice-hooks';

function App() {
  const [pId, setPId] = useStateCB(0);

  function onProvinceChanged(val) {
    setPId(val, newPID => {
      fetchCities(newPID);
    });
  }
}

你看,callback 的回归,让 Hooks 在处理 state 方面至少保持与 this.setState 同等水平,不至于被吐槽落后了。

好了,Hooks 关于 state 的处理的介绍就告一段落了。

-------- ☕ 建议休息片刻,眺望远方 --------

接下来聊聊 Life Cycle 生命周期

我们都知道,每个组件都有它从生到死的生命周期。在 React 中,用的比较多的是:componentDidMountcomponentDidUpdatecomponentWillUnmount

: React Hooks 也应该有与之对应的 useXXX 方法吧?

我也觉得是,但现实是我们并没有发现官方有提供与之对应的 useXXX 方法。我知道你想说什么,放心,依然可以用官方当前提供的 API 来实现这些生命周期。接下来我们就一个一个来讲解。

  • componentDidMount

该生命周期方法会在组件挂载到DOM树后执行,我们可以利用 useEffect 来达到这个目的,怎么弄呢?看下例子吧

useEffect(() => {
  console.log('Do something, such as fetching data');    
}, [])

传个空数组,代表依赖项不变,所以只会执行一次,所以在组件第一次渲染完毕后执行一次,就相当于 componentDidMount 了

  • componentWillUnmout

在组件即将销毁时会执行该周期函数,那么对应的,我们依然可以利用 useEffect 来达到这个目的,看下例子:

useEffect(() => {
  console.log('Do something, such as fetching data');
  return function() {
      console.log('Do something before destroyed')
  }
}, [])

因为销毁动作在整个生命周期中也只会执行一次,所以我们可以在第一个例子的基础上增加一个返回函数,该函数会在组件销毁的时候执行

  • componentDidUpdate

每当组件的 props,state 变化时,就会执行该周期函数,依然可以使用 useEffect 来达到该目的

useEffect(() => {
  console.log('Do something when props / state changes')  
})

这里并没有提供依赖值,所以在每次渲染后都会执行,类似于 componentDidUpdate。但这里有点小问题,就是在初始化时,这里也会执行,即这里会包括初始化,需要额外写些代码来支持,这里就不展开了。

我们还可以使用 useEffect 来达到 watch 某个 state 或 props 变化的目的。所以你会发现,生命周期被混在了一堆 useEffect 代码中,不是那么的直接了当。

虽然 useEffect 可以实现各种生命周期方法,但依然还是那个问题,代码的可读性和可维护性很重要。我们仍然可以用 nice-hooks ,使用它的 useLifeCycle 方法来让生命周期方法回归,用法非常简单,代码一目了然,就不啰嗦了,免得有凑字数嫌疑

useLifeCycle({
  didMount() {
    console.log('Do something, such as fetching data');
  },
  didUpdate() {
    console.log('Do something when props / state changes')   
  },
  willUnmount() {
    console.log('Do something before destroyed')  
  }
});

另外,class 组件写法的生命周期方法有个小缺陷,就是当需要在注销时销毁初始化时声明的一些东西,如事件监听,如定时器,注册和销毁的逻辑被强行拆开,容易疏忽而导致Bug,所以 useLifeCycle 提供了一个 didMountAndWillUnmount 配置来写成对的逻辑,如下

useLifeCycle({
    didMountAndUnmount: [
      {
          didMount() {
              console.log('register foo event)
          },
          willUnmount() {
              console.log('unregister foo event)
          }
      },
      {
          didMount() {
              console.log('register bar event)
          },
          willUnmount() {
              console.log('unregister bar event)
          }
      }
    ]
})

那么推荐的做法就是需要写成对逻辑的写在 didMountAndWillUnmount,而不需要作销毁处理的初始化操作就写在 didMount,willUnmount这里就写一些没有成对逻辑的销毁操作

-------- ☕ 建议休息片刻,听听音乐 --------

最后谈谈 Instance Variables 实例变量

在使用 Hooks 写组件时,因为现在是纯函数组件,所以没法像 class 一样声明实例变量,以下使用变量是会有问题的

function comp() {
    let name = 'daniel';
}
: 看着挺正常的啊,函数内声明变量再正常不过的了。有啥问题呢?

你可能会在某个地方修改了 name 的值,并希望使用 name 变量时它的值是最后修改的值。可惜事与愿违,因为每次组件重新渲染时,渲染函数都会重新执行,那该变量就会被重新初始化。

: 好像是哦,现在就只剩下一个 render 函数,每次都会重新执行,那昨办呢?

我们可以利用 useRef 这个官方 hook,它的 current 属性会一直保持最后的值,如下:

function comp() {
  const nameRef = useRef('daniel');

  function someFn() {
    // get
    let name = nameRef.current;
    // set
    nameRef.current = 'sarah';
  }
}

一旦我们修改了 current 属性的值,那么下次重新渲染时,该 current 值会保持最后修改的值,即达到了实例变量的效果。

: 按照上面的套路,你肯定会说代码可读性不太好之类的

哎呀,台词都被你抢了。但我仍然还是要说,虽然这样利用 useRef 是达到了目的,但代码看起来总是不那么友好。

这里依然建议使用 nice-hooks,它的 useInstanceVar hook,用法与 useState 一样,差别就是 setXXX 的时候只是改变实例变量的值而已,并不会导致重新渲染,示例如下:

function comp() {
  const [nameVar, setNameVar] = useInstanceVar('daniel');

  function someFn() {
    // get
    nameVar;
    // set
    setNameVar('sarah');
  }
}

建议在声明变量名时,使用 Var 作为后缀以区分开 state,如 [ xxxVar, setXXXVar ]

====== 华丽丽的结束分割线 ======

以上都是使用 nice-hooks 这个第三方开源库来使 React Hooks 更好用。

如果你有好的建议,请多给这个开源项目提提issues;

如果觉得对你有用,那还请给这个开源项目加个星呗。

感谢阅读!

阅读 843

ncform / ncgen / nice-hooks 作者

307 声望
2 粉丝
0 条评论

ncform / ncgen / nice-hooks 作者

307 声望
2 粉丝
文章目录
宣传栏