函数化的React Hooks

3,631 阅读5分钟

文章首发于:github.com/USTB-musion…

写在前面

在2018年React Conf大会上,React团队讲解了目前大家使用React开发过程存在的一些问题并推出了一个令人激动的概念:React Hooks,并介绍hooks如何解决这些问题。刚好我所在的团队前端使用的是react+ts+node这套技术栈,前段时间将react版本升级到了16.8,在这个过程中使用到了hooks的功能。在这里写下自己的感受和见解。

本文将从以下几部分进行总结:

  1. React Hooks之前还存在哪些痛点?
  2. React Hooks带来的代码模式改变
  3. 总结
  4. 扩展阅读

React Hooks之前还存在哪些痛点?

1.组件间相似的逻辑复用难

在react hooks问世之前,react能提供给组件间复用逻辑采用的是mixin,但这种方式弊端非常多,所以大家基本都是用render props(渲染属性)和HOC(高阶组件)来复用组件间的逻辑。但这两种方式的缺点也是非常明显的,很容易产生「wrapper hell」,即组件很容易臃肿,增加了debugger的成本。

2.生命周期的问题

在稍微复杂的业务逻辑当中,生命周期是绕不过去的问题。有很多场景下在componentDidmount处理的逻辑不得不又在componentDidUpdate重新处理一次,业务被分散在各个不同的生命周期函数中。

而Hooks的出现,从面向生命周期编程转变为面向业务逻辑编程

3.对人以及机器都不友好的class

虽然Hooks之前可以编写纯函数组件,但它只能是无状态的。非常常见的情况是你编写了一个纯函数组件,但过一段时间你发现得加上一个状态,这时候不得不将纯函数组件改为类组件。这时候得处理this的问题,而且编译器对class也是不友好的。

React Hooks带来的代码模式改变

import { useState, useEffect } from 'react' 
function Example(props) {
  // 声明一个新的状态变量"count" 
  const [count, setCount] = useState(0);

  useEffect(() => {
    subscribe(props.number, setCount)
    return () => {
      unsubscribe(props.number)
    }
  })

  return <div>{count}</div>
}

useState会接受一个初始值,然后会返回一个[状态,状态修改器]的二元组。每次重新渲染时,整个函数会重新执行,但是useState会记住上次的值。而且react是根据useState的调用顺序来记得状态归属的:

const Example = () => {
    const [size, setSize] = useState({ width: 100, height: 100 });
    const [count, setCount] = useState(0);   
}

每一次 Example 被渲染,都是第一次 useState 调用获得 size 和 setSize,第二次 useState 调用获得 count 和 setCount。

useEffect是用来处理副作用的,相当于以前的componentDidMount和componentDidUpdate。它可以返回一个函数,用来在组件卸载前调用。所以useEffect聚合了componentDidMount,componentDidUpdate和componentWillUnmount的操作。如果props.number没有改变,我们是不希望在useEffect订阅/取消订阅里重新执行的,为了实现这个操作,只需要传入一个参数即可。

  useEffect(() => {
    subscribe(props.number, setCount)
    return () => {
      unsubscribe(props.number)
    }
  }, [props.number])

它是一个数组,只要这个数组中的每一个值都不发生变化,则useEffect不需要重新执行。

在将react升级之后,生命周期函数迁移到函数组件可以按照以下方式来操作:

  • constructor: 使用 useState 来初始化 state。
  • getDerivedStateFromProps: 函数组件本身可以直接操作。
  • shouldComponentUpdate: 使用 React.memo。
  • render: 函数组件本身。
  • componentDidMount, componentDidUpdate, componentWillUnmount: 使用 useEffect 实现。

在react hooks开发的过程中,共享代码会采取函数组件的形式。约定使用useXXX开头,想重用这些代码,在函数式组件通过调用useXXX即可。

例如我们可以写这样一个useMountLog,在组件mount的时候打印出一条日志:

const useMountLog = (name) => {
    useEffect(() => {
        console.log(`${name} mounted`);    
    });
}

这样一来,所有的函数式组件中都可以通过调用useMountLog来使用这个功能:

const Example = () => {
    useMountLog('Example')
}

再比如可以通过写useWindowWidth这样一个自定义hook来反映当前窗口的宽度

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  });
  
  return width;
}

这样就可以在其他函数式组件中使用:

function MyResponsiveComponent() {
  const width = useWindowWidth();
  return (
    <p>Window width is {width}</p>
  );
}

在上边的例子可以看到,react hooks大大地减少我们的代码量。而hooks只能在函数式组件中使用,所以可以预测虽然现在react仍然支持类组件,但以后类组件会慢慢得消亡。

总结

React Hooks的出现,将大大地减少react的代码量,解决原来使用react开发遇到的一些问题,也给开发带来了一些变化。包括:

  • react hooks将改变组件间重用代码的方式。使用自定义 hooks,没有 mixin 带来的混乱,没有 HOC 带来的层级深渊。
  • 从面向生命周期编程到面向业务逻辑编程
  • 不再需要class,不再需要关注this等问题

最后,引用 Dan Abramov 在React Conf 2018 上的演讲词结束本文:

“……我曾经疑惑,React 的 Logo 为什么是一个原子?后来我想到了这个解释。我们知道物质由原子组成,是原子的特性决定了物质的外观和行为。就像 React,你可以把用户视图拆成独立的组件,再像原子一样自由组合起来,是组件的特性决定了用户视图的行为。科学家们曾一度认为原子是不可分割的最小单位,直到发现了电子,一种原子内部更小的粒子。事实上,是电子的特征影响了原子的性质。我认为 hooks 就好比电子,与其说它是一个新特性,不如说是已知的 React 特性(state,context,生命周期)的更直接的展现形式,而这四年来我们却一直对它视而不见。

如果盯着 React 的 logo 看的话,你会发现 hooks 其实一直都在。”

扩展阅读

Making Sense of React Hooks

React Today and Tomorrow and 90% Cleaner React

对React Hooks的一些思考

你可以关注我的公众号「慕晨同学」,鹅厂码农,平常记录一些鸡毛蒜皮的点滴,技术,生活,感悟,一起成长。